tokenslim-sdk 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nuoyazhizhou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # tokenslim-sdk
2
+
3
+ [![npm version](https://img.shields.io/npm/v/tokenslim-sdk.svg)](https://www.npmjs.com/package/tokenslim-sdk)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
5
+
6
+ Node.js / TypeScript SDK for [TokenSlim](https://github.com/nuoyazhizhou/tokenslim) — a high-performance Rust compression engine for LLM inputs.
7
+
8
+ Save **50%–95% tokens** on VCS logs, build output, runtime traces, and structured text before sending them to an LLM.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install tokenslim-sdk
14
+ # or
15
+ pnpm add tokenslim-sdk
16
+ # or
17
+ yarn add tokenslim-sdk
18
+ ```
19
+
20
+ Requires Node.js **>= 18**.
21
+
22
+ You also need the TokenSlim server running. Two easy options:
23
+
24
+ ```bash
25
+ # Option A — install the Rust CLI globally
26
+ cargo install tokenslim
27
+ tokenslim serve --port 10086
28
+
29
+ # Option B — Docker
30
+ docker run -d -p 10086:10086 ghcr.io/nuoyazhizhou/tokenslim:latest
31
+ ```
32
+
33
+ ## Quickstart (10 lines)
34
+
35
+ ```ts
36
+ import { TokenSlimClient } from 'tokenslim-sdk';
37
+
38
+ const client = new TokenSlimClient(); // default http://127.0.0.1:10086
39
+ if (await client.isHealthy()) {
40
+ const r = await client.compress(longBuildLog, { preset: 'ai' });
41
+ console.log(`${r.original_tokens} → ${r.compressed_tokens} tokens (saved ${(100 - r.ratio * 100).toFixed(1)}%)`);
42
+ // Later, re-hydrate:
43
+ const original = await client.decompress(r.compressed, r.dictionary ?? {});
44
+ }
45
+ ```
46
+
47
+ That's it. No configuration files, no plugin selection — TokenSlim auto-detects the input type and routes to the right plugin (git log, pytest, gradle, JSON, …).
48
+
49
+ ## API Reference
50
+
51
+ ### `new TokenSlimClient(opts?)`
52
+
53
+ | Option | Default | Description |
54
+ |---|---|---|
55
+ | `host` | `127.0.0.1` | TokenSlim server hostname |
56
+ | `port` | `10086` | TokenSlim server port |
57
+ | `timeoutMs` | `30000` | Per-request timeout |
58
+ | `headers` | `{}` | Extra HTTP headers (auth, etc.) |
59
+
60
+ ### `client.isHealthy(): Promise<boolean>`
61
+
62
+ Pings `GET /health`. Returns `true` only when the server reports `status: UP`.
63
+
64
+ ### `client.compress(text, opts?): Promise<CompressResponse>`
65
+
66
+ Compresses a string. `opts` is optional:
67
+
68
+ ```ts
69
+ {
70
+ preset?: 'ai' | 'balanced' | 'lossless' | string; // default 'balanced'
71
+ plugin_hint?: string; // e.g. 'vcs_git_plugin'
72
+ }
73
+ ```
74
+
75
+ Returns:
76
+
77
+ ```ts
78
+ {
79
+ compressed: string; // compressed output with $P/$T token placeholders
80
+ original_tokens: number; // input token count
81
+ compressed_tokens: number; // output token count
82
+ ratio: number; // compressed_tokens / original_tokens (0.05 = 95% saving)
83
+ plugin_used: string; // 'vcs_git_plugin', 'pytest_plugin', ...
84
+ dictionary?: Record<string, string>; // token → original
85
+ }
86
+ ```
87
+
88
+ ### `client.decompress(compressed, dictionary): Promise<string>`
89
+
90
+ Re-hydrates a compressed string back to its original text. Requires the dictionary returned by `compress()`.
91
+
92
+ ### `client.describe(): Promise<{ version, plugin_count, families }>`
93
+
94
+ Returns server metadata.
95
+
96
+ ### `TokenSlimError`
97
+
98
+ Thrown on network or non-2xx responses. Has `.statusCode` and `.cause` fields.
99
+
100
+ ```ts
101
+ import { TokenSlimClient, TokenSlimError } from 'tokenslim-sdk';
102
+
103
+ try {
104
+ await client.compress(text);
105
+ } catch (e) {
106
+ if (e instanceof TokenSlimError && e.statusCode === 503) {
107
+ console.error('TokenSlim server overloaded');
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Common Recipes
113
+
114
+ ### Wrap a long `git log` for an LLM agent
115
+
116
+ ```ts
117
+ const r = await client.compress(gitLogOutput, { plugin_hint: 'vcs_git_plugin' });
118
+ return `${r.compressed}\n\n[Dictionary]\n${JSON.stringify(r.dictionary)}`;
119
+ ```
120
+
121
+ ### Compress test output and assert savings
122
+
123
+ ```ts
124
+ const r = await client.compress(pytestOutput);
125
+ if (r.ratio > 0.5) {
126
+ throw new Error(`Compression too lossy: ratio=${r.ratio}`);
127
+ }
128
+ ```
129
+
130
+ ### Health-aware retry loop
131
+
132
+ ```ts
133
+ async function compressWithRetry(text: string, maxTries = 3): Promise<CompressResponse> {
134
+ for (let i = 0; i < maxTries; i++) {
135
+ if (await client.isHealthy()) return client.compress(text);
136
+ await new Promise((r) => setTimeout(r, 500 * (i + 1)));
137
+ }
138
+ throw new Error('TokenSlim server unavailable');
139
+ }
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // packages/sdk-nodejs/bin/tokenslim-server.js
3
+ //
4
+ // Node.js wrapper for the standalone `tokenslim-server` Rust binary.
5
+ // Mirrors the resolution logic in bin/tokenslim.js but for the server
6
+ // binary. See that file for the full design notes; this one is the
7
+ // server-only counterpart.
8
+
9
+ const { spawn } = require("node:child_process");
10
+ const fs = require("node:fs");
11
+ const path = require("node:path");
12
+
13
+ const BINARY_NAME =
14
+ process.platform === "win32" ? "tokenslim-server.exe" : "tokenslim-server";
15
+
16
+ const PLATFORM_PKG = {
17
+ "linux:x64": "@tokenslim/cli-binary-linux-x64-gnu",
18
+ "linux:arm64": "@tokenslim/cli-binary-linux-arm64-gnu",
19
+ "darwin:x64": "@tokenslim/cli-binary-darwin-x64",
20
+ "darwin:arm64": "@tokenslim/cli-binary-darwin-arm64",
21
+ "win32:x64": "@tokenslim/cli-binary-windows-x64",
22
+ "win32:arm64": "@tokenslim/cli-binary-windows-arm64",
23
+ };
24
+
25
+ function locateBinary() {
26
+ const key = `${process.platform}:${process.arch}`;
27
+ const pkgName = PLATFORM_PKG[key];
28
+
29
+ if (pkgName) {
30
+ try {
31
+ const pkgPath = require.resolve(`${pkgName}/package.json`, {
32
+ paths: [path.dirname(__filename), path.join(path.dirname(__filename), "..", "..")],
33
+ });
34
+ const binDir = path.join(path.dirname(pkgPath), "bin");
35
+ const candidate = path.join(binDir, BINARY_NAME);
36
+ if (fs.existsSync(candidate)) return candidate;
37
+ } catch {
38
+ // optional dep not installed; fall through.
39
+ }
40
+ }
41
+
42
+ const cached = path.join(__dirname, "..", "vendor", BINARY_NAME);
43
+ if (fs.existsSync(cached)) return cached;
44
+
45
+ return null;
46
+ }
47
+
48
+ function main() {
49
+ const bin = locateBinary();
50
+ if (!bin) {
51
+ const key = `${process.platform}:${process.arch}`;
52
+ process.stderr.write(
53
+ [
54
+ `tokenslim-server: native binary not found for ${key}.`,
55
+ ``,
56
+ `Fix:`,
57
+ ` 1. npm install tokenslim-sdk (avoid --ignore-optional)`,
58
+ ` 2. cargo install tokenslim-server --locked`,
59
+ ` 3. Download from https://github.com/nuoyazhizhou/tokenslim/releases`,
60
+ ``,
61
+ `Tip: the main \`tokenslim\` binary also has a \`serve\` subcommand`,
62
+ `if you only need the server side.`,
63
+ ``,
64
+ ].join("\n"),
65
+ );
66
+ process.exit(127);
67
+ }
68
+
69
+ const args = process.argv.slice(2);
70
+
71
+ const child = spawn(bin, args, {
72
+ stdio: "inherit",
73
+ windowsHide: true,
74
+ });
75
+
76
+ child.on("error", (err) => {
77
+ process.stderr.write(`tokenslim-server: failed to exec ${bin}: ${err.message}\n`);
78
+ process.exit(1);
79
+ });
80
+
81
+ child.on("close", (code, signal) => {
82
+ if (signal) {
83
+ process.kill(process.pid, signal);
84
+ return;
85
+ }
86
+ process.exit(code ?? 0);
87
+ });
88
+ }
89
+
90
+ main();
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // packages/sdk-nodejs/bin/tokenslim.js
3
+ //
4
+ // Node.js wrapper for the `tokenslim` Rust CLI binary.
5
+ //
6
+ // Why a wrapper? We use npm `optionalDependencies` (per-platform packages
7
+ // like @tokenslim/cli-binary-linux-x64-gnu) so the platform-matching binary
8
+ // is installed into node_modules. This wrapper locates that binary at
9
+ // runtime, ensures it is executable, and re-execs it with the original
10
+ // argv. The end user just sees a normal CLI: `tokenslim --version`,
11
+ // `tokenslim run -- git status`, etc.
12
+ //
13
+ // Resolution order:
14
+ // 1. The platform-specific @tokenslim/cli-binary-* package (preferred;
15
+ // ships the binary + 60+ plugin configs).
16
+ // 2. A binary cached by scripts/postinstall.js (GitHub Releases fallback).
17
+ // 3. A system `tokenslim` on PATH (developer escape hatch).
18
+
19
+ const { spawn } = require("node:child_process");
20
+ const fs = require("node:fs");
21
+ const path = require("node:path");
22
+
23
+ const BINARY_NAME = process.platform === "win32" ? "tokenslim.exe" : "tokenslim";
24
+
25
+ // Map Node's (platform, arch) → optionalDependency package name we ship.
26
+ const PLATFORM_PKG = {
27
+ "linux:x64": "@tokenslim/cli-binary-linux-x64-gnu",
28
+ "linux:arm64": "@tokenslim/cli-binary-linux-arm64-gnu",
29
+ "darwin:x64": "@tokenslim/cli-binary-darwin-x64",
30
+ "darwin:arm64": "@tokenslim/cli-binary-darwin-arm64",
31
+ "win32:x64": "@tokenslim/cli-binary-windows-x64",
32
+ "win32:arm64": "@tokenslim/cli-binary-windows-arm64",
33
+ };
34
+
35
+ /**
36
+ * Locate the tokenslim binary, trying (1) the platform-specific package,
37
+ * (2) the postinstall cache, (3) PATH. Returns absolute path or null.
38
+ */
39
+ function locateBinary() {
40
+ const key = `${process.platform}:${process.arch}`;
41
+ const pkgName = PLATFORM_PKG[key];
42
+
43
+ // (1) Per-platform optional dep, resolved from this file's location so it
44
+ // works whether the package is installed globally or locally.
45
+ if (pkgName) {
46
+ try {
47
+ // require.resolve walks node_modules upward; throws if not installed.
48
+ const pkgPath = require.resolve(`${pkgName}/package.json`, {
49
+ paths: [path.dirname(__filename), path.join(path.dirname(__filename), "..", "..")],
50
+ });
51
+ const binDir = path.join(path.dirname(pkgPath), "bin");
52
+ const candidate = path.join(binDir, BINARY_NAME);
53
+ if (fs.existsSync(candidate)) return candidate;
54
+ } catch {
55
+ // optional dep not installed (e.g. yarn with --ignore-optional); fall through.
56
+ }
57
+ }
58
+
59
+ // (2) Cache populated by scripts/postinstall.js (GitHub Releases download).
60
+ const cached = path.join(__dirname, "..", "vendor", BINARY_NAME);
61
+ if (fs.existsSync(cached)) return cached;
62
+
63
+ // (3) System PATH (developer / test mode).
64
+ return null;
65
+ }
66
+
67
+ function main() {
68
+ const bin = locateBinary();
69
+ if (!bin) {
70
+ const key = `${process.platform}:${process.arch}`;
71
+ const pkg = PLATFORM_PKG[key];
72
+ process.stderr.write(
73
+ [
74
+ `tokenslim: native binary not found for ${key}.`,
75
+ pkg
76
+ ? `Tried optional package "${pkg}" and the postinstall cache.`
77
+ : `No prebuilt package for ${key}.`,
78
+ ``,
79
+ `Fix:`,
80
+ pkg
81
+ ? ` 1. Re-run: npm install tokenslim-sdk (avoid --ignore-optional)`
82
+ : ` 1. Use a supported platform, OR`,
83
+ ` 2. Install Rust and run: cargo install tokenslim --locked`,
84
+ ` 3. Or download from https://github.com/nuoyazhizhou/tokenslim/releases`,
85
+ ``,
86
+ ].join("\n"),
87
+ );
88
+ process.exit(127);
89
+ }
90
+
91
+ // Forward argv[2..] (skip `node`, the wrapper path).
92
+ const args = process.argv.slice(2);
93
+
94
+ const child = spawn(bin, args, {
95
+ stdio: "inherit",
96
+ // Detach so signals (Ctrl-C) propagate to the child correctly on all
97
+ // platforms. Without this, the wrapper often swallows SIGINT.
98
+ windowsHide: true,
99
+ });
100
+
101
+ child.on("error", (err) => {
102
+ process.stderr.write(`tokenslim: failed to exec ${bin}: ${err.message}\n`);
103
+ process.exit(1);
104
+ });
105
+
106
+ // Forward exit code. Use `close` rather than `exit` so stdio streams are
107
+ // fully flushed before we hand control back to the shell.
108
+ child.on("close", (code, signal) => {
109
+ if (signal) {
110
+ // Re-raise the signal so the parent shell sees the right status.
111
+ process.kill(process.pid, signal);
112
+ return;
113
+ }
114
+ process.exit(code ?? 0);
115
+ });
116
+ }
117
+
118
+ main();
@@ -0,0 +1,94 @@
1
+ /**
2
+ * TokenSlim Node.js / TypeScript SDK
3
+ *
4
+ * Lightweight REST client for the TokenSlim server.
5
+ * Default endpoint: http://127.0.0.1:10086
6
+ *
7
+ * @see https://github.com/nuoyazhizhou/tokenslim
8
+ */
9
+ /** Health-check response payload from `GET /health`. */
10
+ export interface HealthResponse {
11
+ status: 'UP' | 'DOWN';
12
+ version?: string;
13
+ plugin_count?: number;
14
+ }
15
+ /** Compression request body for `POST /compress`. */
16
+ export interface CompressRequest {
17
+ text: string;
18
+ preset?: 'ai' | 'balanced' | 'lossless' | string;
19
+ plugin_hint?: string;
20
+ }
21
+ /** Compression result. */
22
+ export interface CompressResponse {
23
+ compressed: string;
24
+ original_tokens: number;
25
+ compressed_tokens: number;
26
+ ratio: number;
27
+ plugin_used: string;
28
+ /** Dictionary mapping token placeholders (e.g. `$P1`) to original strings. */
29
+ dictionary?: Record<string, string>;
30
+ }
31
+ /** Decompression request body for `POST /decompress`. */
32
+ export interface DecompressRequest {
33
+ compressed: string;
34
+ dictionary: Record<string, string>;
35
+ }
36
+ /** Decompression result. */
37
+ export interface DecompressResponse {
38
+ text: string;
39
+ }
40
+ /** Error thrown when the SDK cannot reach the server or the server returns a non-2xx status. */
41
+ export declare class TokenSlimError extends Error {
42
+ readonly statusCode?: number;
43
+ readonly cause?: unknown;
44
+ constructor(message: string, opts?: {
45
+ statusCode?: number;
46
+ cause?: unknown;
47
+ });
48
+ }
49
+ /** SDK client options. */
50
+ export interface TokenSlimClientOptions {
51
+ host?: string;
52
+ port?: number;
53
+ /** Request timeout in milliseconds. Default 30000. */
54
+ timeoutMs?: number;
55
+ /** Extra HTTP headers (e.g. authorization). */
56
+ headers?: Record<string, string>;
57
+ }
58
+ /**
59
+ * TokenSlim REST API client.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * import { TokenSlimClient } from 'tokenslim-sdk';
64
+ *
65
+ * const client = new TokenSlimClient();
66
+ * if (await client.isHealthy()) {
67
+ * const r = await client.compress('git status output...');
68
+ * console.log(`${r.original_tokens} → ${r.compressed_tokens} (${(r.ratio * 100).toFixed(1)}%)`);
69
+ * }
70
+ * ```
71
+ */
72
+ export declare class TokenSlimClient {
73
+ private readonly host;
74
+ private readonly port;
75
+ private readonly timeoutMs;
76
+ private readonly headers;
77
+ constructor(opts?: TokenSlimClientOptions);
78
+ /** Ping the server. Returns true if `GET /health` returns `status: UP`. */
79
+ isHealthy(): Promise<boolean>;
80
+ /** Compress a text blob via the server. */
81
+ compress(text: string, opts?: Partial<CompressRequest>): Promise<CompressResponse>;
82
+ /** Re-hydrate a compressed string back to the original text using its dictionary. */
83
+ decompress(compressed: string, dictionary: Record<string, string>): Promise<string>;
84
+ /** Get plugin registry metadata (count, families, available presets). */
85
+ describe(): Promise<{
86
+ version: string;
87
+ plugin_count: number;
88
+ families: string[];
89
+ }>;
90
+ /** Underlying HTTP helper. */
91
+ private request;
92
+ }
93
+ export default TokenSlimClient;
94
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,0BAA0B;AAC1B,MAAM,WAAW,gBAAgB;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,4BAA4B;AAC5B,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,gGAAgG;AAChG,qBAAa,cAAe,SAAQ,KAAK;IACrC,SAAgB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpC,SAAgB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEpB,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAMnF;AAED,0BAA0B;AAC1B,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,eAAe;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAErC,IAAI,GAAE,sBAA2B;IAO7C,2EAA2E;IACrE,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IASnC,2CAA2C;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,OAAO,CAAC,eAAe,CAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQ5F,qFAAqF;IAC/E,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAMzF,yEAAyE;IACnE,QAAQ,IAAI,OAAO,CAAC;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACtB,CAAC;IAIF,8BAA8B;YAChB,OAAO;CAyDxB;AAED,eAAe,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /**
3
+ * TokenSlim Node.js / TypeScript SDK
4
+ *
5
+ * Lightweight REST client for the TokenSlim server.
6
+ * Default endpoint: http://127.0.0.1:10086
7
+ *
8
+ * @see https://github.com/nuoyazhizhou/tokenslim
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.TokenSlimClient = exports.TokenSlimError = void 0;
45
+ const http = __importStar(require("http"));
46
+ const url_1 = require("url");
47
+ /** Error thrown when the SDK cannot reach the server or the server returns a non-2xx status. */
48
+ class TokenSlimError extends Error {
49
+ constructor(message, opts = {}) {
50
+ super(message);
51
+ this.name = 'TokenSlimError';
52
+ this.statusCode = opts.statusCode;
53
+ this.cause = opts.cause;
54
+ }
55
+ }
56
+ exports.TokenSlimError = TokenSlimError;
57
+ /**
58
+ * TokenSlim REST API client.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { TokenSlimClient } from 'tokenslim-sdk';
63
+ *
64
+ * const client = new TokenSlimClient();
65
+ * if (await client.isHealthy()) {
66
+ * const r = await client.compress('git status output...');
67
+ * console.log(`${r.original_tokens} → ${r.compressed_tokens} (${(r.ratio * 100).toFixed(1)}%)`);
68
+ * }
69
+ * ```
70
+ */
71
+ class TokenSlimClient {
72
+ constructor(opts = {}) {
73
+ this.host = opts.host ?? '127.0.0.1';
74
+ this.port = opts.port ?? 10086;
75
+ this.timeoutMs = opts.timeoutMs ?? 30000;
76
+ this.headers = opts.headers ?? {};
77
+ }
78
+ /** Ping the server. Returns true if `GET /health` returns `status: UP`. */
79
+ async isHealthy() {
80
+ try {
81
+ const res = await this.request('GET', '/health');
82
+ return res.status === 'UP';
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ /** Compress a text blob via the server. */
89
+ async compress(text, opts = {}) {
90
+ if (typeof text !== 'string') {
91
+ throw new TokenSlimError('compress() expects a string input');
92
+ }
93
+ const body = { text, ...opts };
94
+ return this.request('POST', '/compress', body);
95
+ }
96
+ /** Re-hydrate a compressed string back to the original text using its dictionary. */
97
+ async decompress(compressed, dictionary) {
98
+ const body = { compressed, dictionary };
99
+ const res = await this.request('POST', '/decompress', body);
100
+ return res.text;
101
+ }
102
+ /** Get plugin registry metadata (count, families, available presets). */
103
+ async describe() {
104
+ return this.request('GET', '/describe');
105
+ }
106
+ /** Underlying HTTP helper. */
107
+ async request(method, path, body) {
108
+ const payload = body !== undefined ? JSON.stringify(body) : undefined;
109
+ const url = new url_1.URL(`http://${this.host}:${this.port}${path}`);
110
+ const headers = {
111
+ Accept: 'application/json',
112
+ ...this.headers,
113
+ };
114
+ if (payload) {
115
+ headers['Content-Type'] = 'application/json';
116
+ headers['Content-Length'] = Buffer.byteLength(payload).toString();
117
+ }
118
+ return new Promise((resolve, reject) => {
119
+ const req = http.request({
120
+ hostname: url.hostname,
121
+ port: url.port,
122
+ path: url.pathname,
123
+ method,
124
+ headers,
125
+ }, (res) => {
126
+ const chunks = [];
127
+ res.on('data', (c) => chunks.push(c));
128
+ res.on('end', () => {
129
+ const raw = Buffer.concat(chunks).toString('utf8');
130
+ const status = res.statusCode ?? 0;
131
+ if (status < 200 || status >= 300) {
132
+ reject(new TokenSlimError(`TokenSlim ${method} ${path} failed: HTTP ${status} ${raw.slice(0, 256)}`, { statusCode: status }));
133
+ return;
134
+ }
135
+ try {
136
+ resolve(JSON.parse(raw));
137
+ }
138
+ catch (e) {
139
+ reject(new TokenSlimError(`Invalid JSON from ${method} ${path}: ${raw.slice(0, 256)}`, {
140
+ cause: e,
141
+ }));
142
+ }
143
+ });
144
+ });
145
+ req.setTimeout(this.timeoutMs, () => {
146
+ req.destroy(new TokenSlimError(`Timeout after ${this.timeoutMs}ms calling ${method} ${path}`));
147
+ });
148
+ req.on('error', (e) => reject(new TokenSlimError(`Network error: ${e.message}`, { cause: e })));
149
+ if (payload)
150
+ req.write(payload);
151
+ req.end();
152
+ });
153
+ }
154
+ }
155
+ exports.TokenSlimClient = TokenSlimClient;
156
+ exports.default = TokenSlimClient;
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "tokenslim-sdk",
3
+ "version": "0.2.0",
4
+ "description": "Node.js / TypeScript SDK for TokenSlim — REST client for compressing LLM inputs (also ships the `tokenslim` / `tokenslim-server` binaries for one-stop install).",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "tokenslim": "bin/tokenslim.js",
9
+ "tokenslim-server": "bin/tokenslim-server.js"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "src/",
14
+ "bin/",
15
+ "scripts/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "clean": "rimraf dist",
22
+ "prepublishOnly": "npm run clean && npm run build",
23
+ "postinstall": "node scripts/postinstall.js",
24
+ "test": "node --test dist/__tests__/*.test.js"
25
+ },
26
+ "keywords": [
27
+ "tokenslim",
28
+ "llm",
29
+ "compression",
30
+ "tokens",
31
+ "ai-agents",
32
+ "log-compression",
33
+ "cli"
34
+ ],
35
+ "author": "nuoyazhizhou",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/nuoyazhizhou/tokenslim.git",
40
+ "directory": "packages/sdk-nodejs"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/nuoyazhizhou/tokenslim/issues"
44
+ },
45
+ "homepage": "https://github.com/nuoyazhizhou/tokenslim#readme",
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
49
+ "optionalDependencies": {
50
+ "@tokenslim/cli-binary-linux-x64-gnu": "0.2.0",
51
+ "@tokenslim/cli-binary-linux-arm64-gnu": "0.2.0",
52
+ "@tokenslim/cli-binary-darwin-x64": "0.2.0",
53
+ "@tokenslim/cli-binary-darwin-arm64": "0.2.0",
54
+ "@tokenslim/cli-binary-windows-x64": "0.2.0",
55
+ "@tokenslim/cli-binary-windows-arm64": "0.2.0"
56
+ },
57
+ "devDependencies": {
58
+ "typescript": "^5.4.0",
59
+ "@types/node": "^20.0.0",
60
+ "rimraf": "^5.0.0"
61
+ }
62
+ }
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ // packages/sdk-nodejs/scripts/postinstall.js
3
+ //
4
+ // npm `postinstall` hook. Runs after `npm install tokenslim-sdk`.
5
+ //
6
+ // What we do, in order:
7
+ // 1. If the matching @tokenslim/cli-binary-* optional package is already
8
+ // on disk (npm/yarn/pnpm usually pick the right one automatically),
9
+ // we just `chmod +x` the binaries and exit. This is the common case.
10
+ // 2. If no optional package is present (e.g. user passed
11
+ // `--ignore-optional`, or we run on an unsupported platform), we try
12
+ // to download a matching tarball from the GitHub Release tagged
13
+ // "v<package-version>". This requires network access.
14
+ // 3. If the download also fails, we log a friendly hint and exit 0.
15
+ // We intentionally don't fail the install — the SDK JS still works
16
+ // as a REST client even without the native binary; users who only
17
+ // need the SDK and have a server elsewhere can ignore the warning.
18
+
19
+ const fs = require("node:fs");
20
+ const path = require("node:path");
21
+ const https = require("node:https");
22
+
23
+ const PKG_VERSION = require("../package.json").version;
24
+ const PACKAGE_NAME = "tokenslim-sdk";
25
+
26
+ // Map (platform, arch) → optional-dependency package name.
27
+ const PLATFORM_PKG = {
28
+ "linux:x64": "@tokenslim/cli-binary-linux-x64-gnu",
29
+ "linux:arm64": "@tokenslim/cli-binary-linux-arm64-gnu",
30
+ "darwin:x64": "@tokenslim/cli-binary-darwin-x64",
31
+ "darwin:arm64": "@tokenslim/cli-binary-darwin-arm64",
32
+ "win32:x64": "@tokenslim/cli-binary-windows-x64",
33
+ "win32:arm64": "@tokenslim/cli-binary-windows-arm64",
34
+ };
35
+
36
+ function info(msg) {
37
+ process.stdout.write(`[${PACKAGE_NAME}/postinstall] ${msg}\n`);
38
+ }
39
+
40
+ function warn(msg) {
41
+ process.stderr.write(`[${PACKAGE_NAME}/postinstall] WARN: ${msg}\n`);
42
+ }
43
+
44
+ function vendorDir() {
45
+ return path.join(__dirname, "..", "vendor");
46
+ }
47
+
48
+ function chmodX(file) {
49
+ if (process.platform === "win32") return; // no-op on Windows
50
+ try {
51
+ fs.chmodSync(file, 0o755);
52
+ } catch (e) {
53
+ warn(`chmod +x failed for ${file}: ${e.message}`);
54
+ }
55
+ }
56
+
57
+ function findOptionalBinary() {
58
+ const key = `${process.platform}:${process.arch}`;
59
+ const pkgName = PLATFORM_PKG[key];
60
+ if (!pkgName) return null;
61
+ try {
62
+ const pkgJson = require.resolve(`${pkgName}/package.json`, {
63
+ paths: [
64
+ path.join(__dirname, "..", "..", ".."),
65
+ path.join(__dirname, "..", ".."),
66
+ ],
67
+ });
68
+ const binDir = path.join(path.dirname(pkgJson), "bin");
69
+ if (!fs.existsSync(binDir)) return null;
70
+ return { pkgName, binDir };
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Stream a URL to a local file via the built-in `https` module — no
78
+ * external deps. Resolves on 2xx, rejects on non-2xx / network error.
79
+ */
80
+ function downloadToFile(url, dest, redirectsLeft = 5) {
81
+ return new Promise((resolve, reject) => {
82
+ const req = https.get(url, (res) => {
83
+ // Handle GitHub Release CDN redirects (302 → S3).
84
+ if (
85
+ res.statusCode &&
86
+ res.statusCode >= 300 &&
87
+ res.statusCode < 400 &&
88
+ res.headers.location
89
+ ) {
90
+ if (redirectsLeft <= 0) {
91
+ reject(new Error("too many redirects"));
92
+ return;
93
+ }
94
+ res.resume();
95
+ downloadToFile(res.headers.location, dest, redirectsLeft - 1)
96
+ .then(resolve)
97
+ .catch(reject);
98
+ return;
99
+ }
100
+ if (!res.statusCode || res.statusCode >= 400) {
101
+ reject(new Error(`HTTP ${res.statusCode} for ${url}`));
102
+ return;
103
+ }
104
+ const file = fs.createWriteStream(dest);
105
+ res.pipe(file);
106
+ file.on("finish", () => file.close(() => resolve(dest)));
107
+ file.on("error", reject);
108
+ });
109
+ req.on("error", reject);
110
+ req.setTimeout(60_000, () => req.destroy(new Error("download timeout")));
111
+ });
112
+ }
113
+
114
+ async function downloadFromGitHubRelease(binName) {
115
+ const tag = `v${PKG_VERSION}`;
116
+ const archive = `${binName}.tar.gz`;
117
+ const url = `https://github.com/nuoyazhizhou/tokenslim/releases/download/${tag}/${archive}`;
118
+ const out = path.join(vendorDir(), binName);
119
+
120
+ info(`downloading ${url}`);
121
+ await downloadToFile(url, out);
122
+ chmodX(out);
123
+ info(`saved → ${out}`);
124
+ }
125
+
126
+ function alreadyCached(binName) {
127
+ const p = path.join(vendorDir(), binName);
128
+ return fs.existsSync(p);
129
+ }
130
+
131
+ async function tryGithubReleaseFallback() {
132
+ const key = `${process.platform}:${process.arch}`;
133
+ if (!PLATFORM_PKG[key]) return false; // unsupported platform
134
+
135
+ const isWin = process.platform === "win32";
136
+ const tokenslim = isWin ? "tokenslim.exe" : "tokenslim";
137
+ const server = isWin ? "tokenslim-server.exe" : "tokenslim-server";
138
+
139
+ if (alreadyCached(tokenslim) && alreadyCached(server)) return true;
140
+
141
+ try {
142
+ if (!alreadyCached(tokenslim)) {
143
+ await downloadFromGitHubRelease(tokenslim);
144
+ }
145
+ if (!alreadyCached(server)) {
146
+ await downloadFromGitHubRelease(server);
147
+ }
148
+ return true;
149
+ } catch (e) {
150
+ warn(`GitHub Release fallback failed: ${e.message}`);
151
+ warn(
152
+ `the SDK JS still works as a REST client; the \`tokenslim\` / \`tokenslim-server\` commands will be unavailable.`,
153
+ );
154
+ return false;
155
+ }
156
+ }
157
+
158
+ async function main() {
159
+ // (1) The matching optional package brought the binary along.
160
+ const found = findOptionalBinary();
161
+ if (found) {
162
+ info(`platform package found: ${found.pkgName}`);
163
+ const dir = found.binDir;
164
+ for (const name of fs.readdirSync(dir)) {
165
+ const p = path.join(dir, name);
166
+ const st = fs.statSync(p);
167
+ if (st.isFile()) chmodX(p);
168
+ }
169
+ info(`binaries ready in ${dir}`);
170
+ return;
171
+ }
172
+
173
+ // (2) GitHub Release fallback. We only attempt this for matching
174
+ // (platform, arch) so users on unusual platforms (FreeBSD, etc.) just
175
+ // see a clean warning instead of a download error.
176
+ info(
177
+ `no platform-specific package for ${process.platform}/${process.arch}; trying GitHub Release v${PKG_VERSION} ...`,
178
+ );
179
+ fs.mkdirSync(vendorDir(), { recursive: true });
180
+ const ok = await tryGithubReleaseFallback();
181
+ if (!ok) {
182
+ warn(
183
+ `skipping binary install; \`tokenslim\` CLI commands won't be available. SDK HTTP client works normally.`,
184
+ );
185
+ }
186
+ }
187
+
188
+ main().catch((e) => {
189
+ warn(`unexpected error: ${e && e.stack ? e.stack : e}`);
190
+ // Never fail npm install on postinstall errors. The user gets the JS
191
+ // SDK and a clear warning; they can rerun with --foreground-scripts
192
+ // to see logs and re-attempt.
193
+ process.exit(0);
194
+ });
package/src/index.ts ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * TokenSlim Node.js / TypeScript SDK
3
+ *
4
+ * Lightweight REST client for the TokenSlim server.
5
+ * Default endpoint: http://127.0.0.1:10086
6
+ *
7
+ * @see https://github.com/nuoyazhizhou/tokenslim
8
+ */
9
+
10
+ import * as http from 'http';
11
+ import { URL } from 'url';
12
+
13
+ /** Health-check response payload from `GET /health`. */
14
+ export interface HealthResponse {
15
+ status: 'UP' | 'DOWN';
16
+ version?: string;
17
+ plugin_count?: number;
18
+ }
19
+
20
+ /** Compression request body for `POST /compress`. */
21
+ export interface CompressRequest {
22
+ text: string;
23
+ preset?: 'ai' | 'balanced' | 'lossless' | string;
24
+ plugin_hint?: string;
25
+ }
26
+
27
+ /** Compression result. */
28
+ export interface CompressResponse {
29
+ compressed: string;
30
+ original_tokens: number;
31
+ compressed_tokens: number;
32
+ ratio: number;
33
+ plugin_used: string;
34
+ /** Dictionary mapping token placeholders (e.g. `$P1`) to original strings. */
35
+ dictionary?: Record<string, string>;
36
+ }
37
+
38
+ /** Decompression request body for `POST /decompress`. */
39
+ export interface DecompressRequest {
40
+ compressed: string;
41
+ dictionary: Record<string, string>;
42
+ }
43
+
44
+ /** Decompression result. */
45
+ export interface DecompressResponse {
46
+ text: string;
47
+ }
48
+
49
+ /** Error thrown when the SDK cannot reach the server or the server returns a non-2xx status. */
50
+ export class TokenSlimError extends Error {
51
+ public readonly statusCode?: number;
52
+ public readonly cause?: unknown;
53
+
54
+ constructor(message: string, opts: { statusCode?: number; cause?: unknown } = {}) {
55
+ super(message);
56
+ this.name = 'TokenSlimError';
57
+ this.statusCode = opts.statusCode;
58
+ this.cause = opts.cause;
59
+ }
60
+ }
61
+
62
+ /** SDK client options. */
63
+ export interface TokenSlimClientOptions {
64
+ host?: string;
65
+ port?: number;
66
+ /** Request timeout in milliseconds. Default 30000. */
67
+ timeoutMs?: number;
68
+ /** Extra HTTP headers (e.g. authorization). */
69
+ headers?: Record<string, string>;
70
+ }
71
+
72
+ /**
73
+ * TokenSlim REST API client.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { TokenSlimClient } from 'tokenslim-sdk';
78
+ *
79
+ * const client = new TokenSlimClient();
80
+ * if (await client.isHealthy()) {
81
+ * const r = await client.compress('git status output...');
82
+ * console.log(`${r.original_tokens} → ${r.compressed_tokens} (${(r.ratio * 100).toFixed(1)}%)`);
83
+ * }
84
+ * ```
85
+ */
86
+ export class TokenSlimClient {
87
+ private readonly host: string;
88
+ private readonly port: number;
89
+ private readonly timeoutMs: number;
90
+ private readonly headers: Record<string, string>;
91
+
92
+ constructor(opts: TokenSlimClientOptions = {}) {
93
+ this.host = opts.host ?? '127.0.0.1';
94
+ this.port = opts.port ?? 10086;
95
+ this.timeoutMs = opts.timeoutMs ?? 30_000;
96
+ this.headers = opts.headers ?? {};
97
+ }
98
+
99
+ /** Ping the server. Returns true if `GET /health` returns `status: UP`. */
100
+ async isHealthy(): Promise<boolean> {
101
+ try {
102
+ const res = await this.request<HealthResponse>('GET', '/health');
103
+ return res.status === 'UP';
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /** Compress a text blob via the server. */
110
+ async compress(text: string, opts: Partial<CompressRequest> = {}): Promise<CompressResponse> {
111
+ if (typeof text !== 'string') {
112
+ throw new TokenSlimError('compress() expects a string input');
113
+ }
114
+ const body: CompressRequest = { text, ...opts };
115
+ return this.request<CompressResponse>('POST', '/compress', body);
116
+ }
117
+
118
+ /** Re-hydrate a compressed string back to the original text using its dictionary. */
119
+ async decompress(compressed: string, dictionary: Record<string, string>): Promise<string> {
120
+ const body: DecompressRequest = { compressed, dictionary };
121
+ const res = await this.request<DecompressResponse>('POST', '/decompress', body);
122
+ return res.text;
123
+ }
124
+
125
+ /** Get plugin registry metadata (count, families, available presets). */
126
+ async describe(): Promise<{
127
+ version: string;
128
+ plugin_count: number;
129
+ families: string[];
130
+ }> {
131
+ return this.request('GET', '/describe');
132
+ }
133
+
134
+ /** Underlying HTTP helper. */
135
+ private async request<T>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> {
136
+ const payload = body !== undefined ? JSON.stringify(body) : undefined;
137
+ const url = new URL(`http://${this.host}:${this.port}${path}`);
138
+
139
+ const headers: Record<string, string> = {
140
+ Accept: 'application/json',
141
+ ...this.headers,
142
+ };
143
+ if (payload) {
144
+ headers['Content-Type'] = 'application/json';
145
+ headers['Content-Length'] = Buffer.byteLength(payload).toString();
146
+ }
147
+
148
+ return new Promise<T>((resolve, reject) => {
149
+ const req = http.request(
150
+ {
151
+ hostname: url.hostname,
152
+ port: url.port,
153
+ path: url.pathname,
154
+ method,
155
+ headers,
156
+ },
157
+ (res) => {
158
+ const chunks: Buffer[] = [];
159
+ res.on('data', (c: Buffer) => chunks.push(c));
160
+ res.on('end', () => {
161
+ const raw = Buffer.concat(chunks).toString('utf8');
162
+ const status = res.statusCode ?? 0;
163
+ if (status < 200 || status >= 300) {
164
+ reject(
165
+ new TokenSlimError(
166
+ `TokenSlim ${method} ${path} failed: HTTP ${status} ${raw.slice(0, 256)}`,
167
+ { statusCode: status },
168
+ ),
169
+ );
170
+ return;
171
+ }
172
+ try {
173
+ resolve(JSON.parse(raw) as T);
174
+ } catch (e) {
175
+ reject(
176
+ new TokenSlimError(`Invalid JSON from ${method} ${path}: ${raw.slice(0, 256)}`, {
177
+ cause: e,
178
+ }),
179
+ );
180
+ }
181
+ });
182
+ },
183
+ );
184
+ req.setTimeout(this.timeoutMs, () => {
185
+ req.destroy(new TokenSlimError(`Timeout after ${this.timeoutMs}ms calling ${method} ${path}`));
186
+ });
187
+ req.on('error', (e) => reject(new TokenSlimError(`Network error: ${e.message}`, { cause: e })));
188
+ if (payload) req.write(payload);
189
+ req.end();
190
+ });
191
+ }
192
+ }
193
+
194
+ export default TokenSlimClient;