@emesoft/tokenhub 0.3.1 → 0.5.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 +7 -2
- package/bin/tokenhub.js +26 -5
- package/lib/admin.d.ts +4 -0
- package/lib/admin.js +51 -0
- package/lib/pair.d.ts +5 -0
- package/lib/pair.js +24 -4
- package/lib/settings.d.ts +14 -1
- package/lib/settings.js +56 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,12 +4,15 @@ Pair Claude Code on this machine with a [TokenHub](https://tokenhub.aimpact.io.v
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npx @emesoft/tokenhub connect https://tokenhub.aimpact.io.vn
|
|
7
|
+
npx @emesoft/tokenhub connect https://tokenhub.aimpact.io.vn --no-statusline
|
|
7
8
|
npx @emesoft/tokenhub usage
|
|
8
9
|
npx @emesoft/tokenhub dashboard
|
|
9
10
|
npx @emesoft/tokenhub disconnect
|
|
11
|
+
npx @emesoft/tokenhub connect https://tokenhub.aimpact.io.vn --admin
|
|
12
|
+
npx @emesoft/tokenhub disconnect --admin
|
|
10
13
|
```
|
|
11
14
|
|
|
12
|
-
`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`). 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.
|
|
15
|
+
`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.
|
|
13
16
|
|
|
14
17
|
`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).
|
|
15
18
|
|
|
@@ -17,7 +20,9 @@ npx @emesoft/tokenhub disconnect
|
|
|
17
20
|
|
|
18
21
|
Requests to the pool carry your key and one extra header, `x-tokenhub-cli: <version>` (this CLI's version), and nothing else.
|
|
19
22
|
|
|
20
|
-
`disconnect` removes only those two entries
|
|
23
|
+
`disconnect` removes only those two entries and the TokenHub `statusLine`.
|
|
24
|
+
|
|
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.
|
|
21
26
|
|
|
22
27
|
If `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_API_KEY` is set in your shell, it overrides `settings.json`.
|
|
23
28
|
|
package/bin/tokenhub.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { adminConfigPath, removeAdminConfig } from "../lib/admin.js";
|
|
3
4
|
import { parseDashboardArgs, runDashboard } from "../lib/dashboard.js";
|
|
4
5
|
import { runConnect } from "../lib/pair.js";
|
|
5
6
|
import { disconnectSettings, readSettings, settingsPath } from "../lib/settings.js";
|
|
@@ -8,11 +9,17 @@ import { fetchUsage, formatUsage, resolveCredentials } from "../lib/usage.js";
|
|
|
8
9
|
const HELP = `tokenhub - pair Claude Code with a TokenHub pool
|
|
9
10
|
|
|
10
11
|
Usage:
|
|
11
|
-
tokenhub connect <poolUrl>
|
|
12
|
+
tokenhub connect <poolUrl> [--no-statusline]
|
|
13
|
+
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
|
|
16
|
+
Pair this machine as an admin device (for the desktop overlay's pool view): the admin
|
|
17
|
+
confirms in the console, the token is saved to ~/.tokenhub/admin.json
|
|
12
18
|
tokenhub usage [--json] Show today's and this week's spend, credit and pool health
|
|
13
19
|
tokenhub dashboard [--interval <sec>] [--once]
|
|
14
20
|
Live full-screen view of spend, pool health and recent requests (q quits)
|
|
15
|
-
tokenhub disconnect Remove the pool entries from settings.json
|
|
21
|
+
tokenhub disconnect Remove the pool entries and the status line from settings.json
|
|
22
|
+
tokenhub disconnect --admin Forget the admin device token on this machine (revoke it on the pool's admin Devices page)
|
|
16
23
|
tokenhub --help | --version
|
|
17
24
|
`;
|
|
18
25
|
|
|
@@ -22,7 +29,9 @@ Usage:
|
|
|
22
29
|
* @returns {Promise<number>} Process exit code.
|
|
23
30
|
*/
|
|
24
31
|
async function main(argv) {
|
|
25
|
-
const [command,
|
|
32
|
+
const [command, ...rest] = argv;
|
|
33
|
+
const flags = rest.filter((item) => item.startsWith("--"));
|
|
34
|
+
const arg = rest.find((item) => !item.startsWith("--"));
|
|
26
35
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
27
36
|
console.log(HELP);
|
|
28
37
|
return command ? 0 : 1;
|
|
@@ -40,7 +49,14 @@ async function main(argv) {
|
|
|
40
49
|
}
|
|
41
50
|
const controller = new AbortController();
|
|
42
51
|
process.once("SIGINT", () => controller.abort());
|
|
43
|
-
return runConnect({
|
|
52
|
+
return runConnect({
|
|
53
|
+
poolUrl: arg,
|
|
54
|
+
settingsFile: file,
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
statusLine: !flags.includes("--no-statusline"),
|
|
57
|
+
admin: flags.includes("--admin"),
|
|
58
|
+
adminFile: adminConfigPath(),
|
|
59
|
+
});
|
|
44
60
|
}
|
|
45
61
|
if (command === "usage") {
|
|
46
62
|
try {
|
|
@@ -50,7 +66,7 @@ async function main(argv) {
|
|
|
50
66
|
return 1;
|
|
51
67
|
}
|
|
52
68
|
const report = await fetchUsage(credentials.baseUrl, credentials.key);
|
|
53
|
-
console.log(
|
|
69
|
+
console.log(flags.includes("--json") ? JSON.stringify(report, null, 2) : formatUsage(report));
|
|
54
70
|
return report.blocked ? 2 : 0;
|
|
55
71
|
} catch (error) {
|
|
56
72
|
console.error(`Error: ${error.message}`);
|
|
@@ -73,6 +89,11 @@ async function main(argv) {
|
|
|
73
89
|
}
|
|
74
90
|
if (command === "disconnect") {
|
|
75
91
|
try {
|
|
92
|
+
if (flags.includes("--admin")) {
|
|
93
|
+
const adminFile = adminConfigPath();
|
|
94
|
+
console.log(removeAdminConfig(adminFile) ? `Admin device forgotten. Removed ${adminFile}` : `Nothing to remove at ${adminFile}`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
76
97
|
console.log(disconnectSettings(file) ? `Disconnected. Updated ${file}` : `Nothing to remove in ${file}`);
|
|
77
98
|
return 0;
|
|
78
99
|
} catch (error) {
|
package/lib/admin.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function adminConfigPath(env?: Record<string, string | undefined>, home?: string): string;
|
|
2
|
+
export function writeAdminConfig(file: string, baseUrl: string, token: string): void;
|
|
3
|
+
export function readAdminConfig(file: string): { baseUrl: string; token: string } | null;
|
|
4
|
+
export function removeAdminConfig(file: string): boolean;
|
package/lib/admin.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Locate the file `tokenhub connect --admin` stores the admin device token in (read by the desktop overlay).
|
|
7
|
+
* @param {Record<string, string | undefined>} [env] Environment to read TOKENHUB_CONFIG_DIR from.
|
|
8
|
+
* @param {string} [home] Home directory used when TOKENHUB_CONFIG_DIR is unset.
|
|
9
|
+
* @returns {string} Absolute path of admin.json.
|
|
10
|
+
*/
|
|
11
|
+
export function adminConfigPath(env = process.env, home = homedir()) {
|
|
12
|
+
return join(env.TOKENHUB_CONFIG_DIR || join(home, ".tokenhub"), "admin.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Store the pool address and admin device token, readable only by the current user where the OS supports it.
|
|
17
|
+
* @param {string} file Path from `adminConfigPath`.
|
|
18
|
+
* @param {string} baseUrl Pool address.
|
|
19
|
+
* @param {string} token Admin device token issued by the pool.
|
|
20
|
+
* @returns {void}
|
|
21
|
+
*/
|
|
22
|
+
export function writeAdminConfig(file, baseUrl, token) {
|
|
23
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
24
|
+
writeFileSync(file, `${JSON.stringify({ baseUrl, token }, null, 2)}\n`, { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read a stored admin device.
|
|
29
|
+
* @param {string} file Path from `adminConfigPath`.
|
|
30
|
+
* @returns {{ baseUrl: string, token: string } | null} The pair, or null when missing or unusable.
|
|
31
|
+
*/
|
|
32
|
+
export function readAdminConfig(file) {
|
|
33
|
+
if (!existsSync(file)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const { baseUrl, token } = JSON.parse(readFileSync(file, "utf8"));
|
|
36
|
+
return typeof baseUrl === "string" && typeof token === "string" && baseUrl && token ? { baseUrl, token } : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Forget the stored admin device (the token stays valid on the pool until revoked on its Devices page).
|
|
44
|
+
* @param {string} file Path from `adminConfigPath`.
|
|
45
|
+
* @returns {boolean} Whether a file was removed.
|
|
46
|
+
*/
|
|
47
|
+
export function removeAdminConfig(file) {
|
|
48
|
+
if (!existsSync(file)) return false;
|
|
49
|
+
rmSync(file);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
package/lib/pair.d.ts
CHANGED
|
@@ -8,4 +8,9 @@ export function runConnect(options: {
|
|
|
8
8
|
deviceName?: string;
|
|
9
9
|
intervalOverrideMs?: number;
|
|
10
10
|
env?: Record<string, string | undefined>;
|
|
11
|
+
statusLine?: boolean;
|
|
12
|
+
/** Pair as an admin device (token goes to `adminFile`, not settings.json). */
|
|
13
|
+
admin?: boolean;
|
|
14
|
+
/** Where the admin device token is stored; required with `admin`. */
|
|
15
|
+
adminFile?: string;
|
|
11
16
|
}): Promise<number>;
|
package/lib/pair.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
|
+
import { writeAdminConfig } from "./admin.js";
|
|
3
4
|
import { connectSettings, normalizeUrl, overridingEnvVars, readSettings } from "./settings.js";
|
|
4
5
|
|
|
5
6
|
const MAX_WAIT_MS = 15 * 60 * 1000;
|
|
@@ -69,6 +70,9 @@ async function postJson(url, body, signal) {
|
|
|
69
70
|
* @param {string} [options.deviceName] Name shown on the confirmation page.
|
|
70
71
|
* @param {number} [options.intervalOverrideMs] Poll interval override (tests).
|
|
71
72
|
* @param {Record<string, string | undefined>} [options.env] Environment inspected for overriding variables.
|
|
73
|
+
* @param {boolean} [options.statusLine] Also install the TokenHub status line (default true).
|
|
74
|
+
* @param {boolean} [options.admin] Pair as an admin device: the admin approves in the console and the token is stored in `adminFile` (for the desktop overlay), leaving Claude Code's settings alone.
|
|
75
|
+
* @param {string} [options.adminFile] Path from `adminConfigPath`; required with `admin`.
|
|
72
76
|
* @returns {Promise<number>} Process exit code.
|
|
73
77
|
*/
|
|
74
78
|
export async function runConnect({
|
|
@@ -80,17 +84,20 @@ export async function runConnect({
|
|
|
80
84
|
deviceName = hostname(),
|
|
81
85
|
intervalOverrideMs,
|
|
82
86
|
env = process.env,
|
|
87
|
+
statusLine = true,
|
|
88
|
+
admin = false,
|
|
89
|
+
adminFile,
|
|
83
90
|
}) {
|
|
84
91
|
let pool;
|
|
85
92
|
try {
|
|
86
93
|
pool = normalizeUrl(poolUrl);
|
|
87
|
-
readSettings(settingsFile);
|
|
94
|
+
if (!admin) readSettings(settingsFile);
|
|
88
95
|
} catch (error) {
|
|
89
96
|
log(`Error: ${error.message}`);
|
|
90
97
|
return 1;
|
|
91
98
|
}
|
|
92
99
|
try {
|
|
93
|
-
const start = await postJson(`${pool}/pair/start`, { deviceName }, signal);
|
|
100
|
+
const start = await postJson(`${pool}/pair/start`, admin ? { deviceName, kind: "admin" } : { deviceName }, signal);
|
|
94
101
|
if (start.status === 429) {
|
|
95
102
|
log("Too many pairing attempts. Wait a few minutes and try again.");
|
|
96
103
|
return 1;
|
|
@@ -103,7 +110,7 @@ export async function runConnect({
|
|
|
103
110
|
log("");
|
|
104
111
|
log(` Your code: ${userCode}`);
|
|
105
112
|
log("");
|
|
106
|
-
log(`Confirm it at: ${verifyUrl}`);
|
|
113
|
+
log(`Confirm it at: ${verifyUrl}${admin ? " (sign in as the admin)" : ""}`);
|
|
107
114
|
log("Opening your browser. Waiting for approval (Ctrl+C to cancel)...");
|
|
108
115
|
open(verifyUrl);
|
|
109
116
|
|
|
@@ -118,9 +125,22 @@ export async function runConnect({
|
|
|
118
125
|
const poll = await postJson(`${pool}/pair/poll`, { deviceCode }, signal);
|
|
119
126
|
if (poll.status === 429) continue;
|
|
120
127
|
const status = poll.body?.status;
|
|
128
|
+
if (status === "approved" && admin) {
|
|
129
|
+
if (poll.body.kind !== "admin") {
|
|
130
|
+
log("Error: the pool did not issue an admin device token. Update the pool, then run the command again.");
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
writeAdminConfig(adminFile, poll.body.baseUrl, poll.body.key);
|
|
134
|
+
log(`Admin device paired. Token saved to ${adminFile}`);
|
|
135
|
+
log(`Revoke it any time on the pool's admin Devices page (${poll.body.baseUrl}/admin/pair).`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
121
138
|
if (status === "approved") {
|
|
122
|
-
connectSettings(settingsFile, poll.body.baseUrl, poll.body.key);
|
|
139
|
+
const statusResult = connectSettings(settingsFile, poll.body.baseUrl, poll.body.key, { statusLine });
|
|
123
140
|
log(`Connected. Claude Code settings updated: ${settingsFile}`);
|
|
141
|
+
if (statusResult === "installed") log("Status line added: today's spend and credit show under the Claude Code prompt.");
|
|
142
|
+
if (statusResult === "updated") log("Status line refreshed with your new key.");
|
|
143
|
+
if (statusResult === "kept") log("Your existing statusLine was left as is. The setup guide on the pool site has the TokenHub one if you want it.");
|
|
124
144
|
const overriding = overridingEnvVars(env);
|
|
125
145
|
if (overriding.length > 0) {
|
|
126
146
|
log(`Warning: ${overriding.join(", ")} set in your shell overrides settings.json. Unset it to use the pool.`);
|
package/lib/settings.d.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
export function settingsPath(env?: Record<string, string | undefined>, home?: string): string;
|
|
2
2
|
export function normalizeUrl(input: string): string;
|
|
3
3
|
export function mergeSettings(settings: Record<string, any>, baseUrl: string, key: string): Record<string, any>;
|
|
4
|
+
export function statusLineCommand(baseUrl: string, key: string, platform?: string): string;
|
|
5
|
+
export function isTokenhubStatusLine(value: unknown): boolean;
|
|
6
|
+
export function installStatusLine(
|
|
7
|
+
settings: Record<string, any>,
|
|
8
|
+
baseUrl: string,
|
|
9
|
+
key: string,
|
|
10
|
+
platform?: string,
|
|
11
|
+
): { settings: Record<string, any>; result: "installed" | "updated" | "kept" };
|
|
4
12
|
export function removeSettings(settings: Record<string, any>): Record<string, any>;
|
|
5
13
|
export function overridingEnvVars(env?: Record<string, string | undefined>): string[];
|
|
6
14
|
export function readSettings(file: string): { exists: boolean; settings: Record<string, any> };
|
|
7
15
|
export function writeSettings(file: string, settings: Record<string, any>): void;
|
|
8
|
-
export function connectSettings(
|
|
16
|
+
export function connectSettings(
|
|
17
|
+
file: string,
|
|
18
|
+
baseUrl: string,
|
|
19
|
+
key: string,
|
|
20
|
+
options?: { statusLine?: boolean; platform?: string },
|
|
21
|
+
): "installed" | "updated" | "kept" | "skipped";
|
|
9
22
|
export function disconnectSettings(file: string): boolean;
|
package/lib/settings.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const BASE_URL_KEY = "ANTHROPIC_BASE_URL";
|
|
6
6
|
const TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN";
|
|
7
|
+
const STATUS_PATH = "/member/api/status";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Locate Claude Code's user settings file.
|
|
@@ -48,15 +49,52 @@ export function mergeSettings(settings, baseUrl, key) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
|
-
*
|
|
52
|
+
* The shell command Claude Code runs to draw the TokenHub status line (today's spend and credit under the prompt).
|
|
53
|
+
* @param {string} baseUrl Pool address.
|
|
54
|
+
* @param {string} key The member's key.
|
|
55
|
+
* @param {string} [platform] Node platform name; Windows gets `curl.exe` with double quotes (works from cmd, PowerShell and Git Bash).
|
|
56
|
+
* @returns {string} The command line.
|
|
57
|
+
*/
|
|
58
|
+
export function statusLineCommand(baseUrl, key, platform = process.platform) {
|
|
59
|
+
const url = `${baseUrl}${STATUS_PATH}?format=line`;
|
|
60
|
+
return platform === "win32" ? `curl.exe -s -H "x-api-key: ${key}" "${url}"` : `curl -s -H 'x-api-key: ${key}' '${url}'`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {unknown} value A `statusLine` setting.
|
|
65
|
+
* @returns {boolean} True when it is the TokenHub status line (a command calling the pool's status endpoint).
|
|
66
|
+
*/
|
|
67
|
+
export function isTokenhubStatusLine(value) {
|
|
68
|
+
return isPlainObject(value) && typeof value.command === "string" && value.command.includes(STATUS_PATH);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Add the TokenHub status line unless the user already has a different one.
|
|
52
73
|
* @param {Record<string, any>} settings Existing settings object.
|
|
53
|
-
* @
|
|
74
|
+
* @param {string} baseUrl Pool address.
|
|
75
|
+
* @param {string} key The member's key.
|
|
76
|
+
* @param {string} [platform] Node platform name.
|
|
77
|
+
* @returns {{ settings: Record<string, any>, result: "installed" | "updated" | "kept" }} `installed` when there was none,
|
|
78
|
+
* `updated` when ours was refreshed with the new key, `kept` when another status line was left alone.
|
|
79
|
+
*/
|
|
80
|
+
export function installStatusLine(settings, baseUrl, key, platform = process.platform) {
|
|
81
|
+
const existing = settings.statusLine;
|
|
82
|
+
if (existing !== undefined && !isTokenhubStatusLine(existing)) return { settings, result: "kept" };
|
|
83
|
+
const statusLine = { ...(isPlainObject(existing) ? existing : {}), type: "command", command: statusLineCommand(baseUrl, key, platform) };
|
|
84
|
+
return { settings: { ...settings, statusLine }, result: existing === undefined ? "installed" : "updated" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Return a copy of the settings without the pool connection entries (and without our status line).
|
|
89
|
+
* @param {Record<string, any>} settings Existing settings object.
|
|
90
|
+
* @returns {Record<string, any>} New settings; `env` is dropped when it ends up empty, another status line is kept.
|
|
54
91
|
*/
|
|
55
92
|
export function removeSettings(settings) {
|
|
56
|
-
const { env, ...rest } = settings;
|
|
57
|
-
|
|
93
|
+
const { env, statusLine, ...rest } = settings;
|
|
94
|
+
const keep = statusLine !== undefined && !isTokenhubStatusLine(statusLine) ? { statusLine } : {};
|
|
95
|
+
if (!isPlainObject(env)) return { ...rest, ...keep, ...(env === undefined ? {} : { env }) };
|
|
58
96
|
const { [BASE_URL_KEY]: _url, [TOKEN_KEY]: _token, ...otherEnv } = env;
|
|
59
|
-
return Object.keys(otherEnv).length > 0 ? { ...rest, env: otherEnv } : rest;
|
|
97
|
+
return Object.keys(otherEnv).length > 0 ? { ...rest, ...keep, env: otherEnv } : { ...rest, ...keep };
|
|
60
98
|
}
|
|
61
99
|
|
|
62
100
|
/**
|
|
@@ -103,20 +141,28 @@ export function writeSettings(file, settings) {
|
|
|
103
141
|
}
|
|
104
142
|
|
|
105
143
|
/**
|
|
106
|
-
* Persist a pool connection into the settings file.
|
|
144
|
+
* Persist a pool connection (and the TokenHub status line) into the settings file.
|
|
107
145
|
* @param {string} file Path of settings.json.
|
|
108
146
|
* @param {string} baseUrl Value for ANTHROPIC_BASE_URL.
|
|
109
147
|
* @param {string} key Value for ANTHROPIC_AUTH_TOKEN.
|
|
110
|
-
* @
|
|
148
|
+
* @param {{ statusLine?: boolean, platform?: string }} [options] `statusLine: false` skips the status line; `platform` is for tests.
|
|
149
|
+
* @returns {"installed" | "updated" | "kept" | "skipped"} What happened to the status line.
|
|
111
150
|
* @throws {Error} When the existing file is invalid JSON (left untouched).
|
|
112
151
|
*/
|
|
113
|
-
export function connectSettings(file, baseUrl, key) {
|
|
152
|
+
export function connectSettings(file, baseUrl, key, options = {}) {
|
|
114
153
|
const { settings } = readSettings(file);
|
|
115
|
-
|
|
154
|
+
const merged = mergeSettings(settings, baseUrl, key);
|
|
155
|
+
if (options.statusLine === false) {
|
|
156
|
+
writeSettings(file, merged);
|
|
157
|
+
return "skipped";
|
|
158
|
+
}
|
|
159
|
+
const { settings: next, result } = installStatusLine(merged, baseUrl, key, options.platform);
|
|
160
|
+
writeSettings(file, next);
|
|
161
|
+
return result;
|
|
116
162
|
}
|
|
117
163
|
|
|
118
164
|
/**
|
|
119
|
-
* Remove the pool connection from the settings file.
|
|
165
|
+
* Remove the pool connection (and our status line) from the settings file.
|
|
120
166
|
* @param {string} file Path of settings.json.
|
|
121
167
|
* @returns {boolean} True when the file changed, false when there was nothing to remove.
|
|
122
168
|
* @throws {Error} When the existing file is invalid JSON (left untouched).
|