klovys99 0.1.0-main.10
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 +21 -0
- package/README.md +403 -0
- package/npm/cli.js +123 -0
- package/npm/lib/configure.js +164 -0
- package/npm/lib/install.js +205 -0
- package/npm/postinstall.js +99 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Korbicorp
|
|
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,403 @@
|
|
|
1
|
+
# klovys99
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
|
|
5
|
+
klovys99 is a local reverse proxy that anonymizes sensitive prompt data before
|
|
6
|
+
forwarding requests to Anthropic or OpenAI APIs.
|
|
7
|
+
|
|
8
|
+
It is designed to sit between coding clients such as Claude Code or Codex and
|
|
9
|
+
their upstream API, replacing detected personal or sensitive values with stable
|
|
10
|
+
pseudonym tokens before the request leaves the machine.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Local reverse proxy for Anthropic and OpenAI-compatible JSON requests.
|
|
15
|
+
- `npm install` workflow that downloads a prebuilt binary for the current OS
|
|
16
|
+
and architecture and exposes a `klovys99` command.
|
|
17
|
+
- Client configuration helpers for Codex and Claude Code.
|
|
18
|
+
- Built-in deterministic detectors for common PII and sensitive identifiers.
|
|
19
|
+
- Dynamic detector loading from the official Gitleaks and Microsoft Presidio
|
|
20
|
+
rule sources.
|
|
21
|
+
- Optional local LLM extraction through Ollama for contextual names, addresses,
|
|
22
|
+
dates, and vehicle plates.
|
|
23
|
+
- Stable pseudonym tokens for the lifetime of the proxy process.
|
|
24
|
+
- Structured logs with anonymization counters instead of raw prompt values.
|
|
25
|
+
- Disk cache for downloaded external rules to avoid repeated network fetches on
|
|
26
|
+
every startup.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Node.js 18 or newer.
|
|
31
|
+
- Network access on first startup to download the default Gitleaks and Presidio
|
|
32
|
+
rule sources.
|
|
33
|
+
- An Anthropic API key, Claude subscription, or OpenAI API key depending on the
|
|
34
|
+
client you route through Klovys99.
|
|
35
|
+
- Ollama, only when `KLOVIS_LLM_ENABLED=true`.
|
|
36
|
+
|
|
37
|
+
Go 1.25 or newer is only required if you work from a source checkout or build
|
|
38
|
+
release binaries yourself.
|
|
39
|
+
|
|
40
|
+
Check your local tooling:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
node -v
|
|
44
|
+
npm -v
|
|
45
|
+
go version
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional LLM mode requires a local Ollama model:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
ollama --version
|
|
52
|
+
ollama pull mistral
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
From the repository root:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npm install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`npm install klovys99` runs a `postinstall` step that downloads the matching
|
|
64
|
+
binary from the GitHub release for the package version into `dist/` and exposes
|
|
65
|
+
the CLI entrypoints `klovys99` and `klovis`. `klovys99` is the preferred name
|
|
66
|
+
and `klovis` remains available for compatibility.
|
|
67
|
+
|
|
68
|
+
Supported prebuilt targets:
|
|
69
|
+
|
|
70
|
+
- macOS `arm64`
|
|
71
|
+
- macOS `x64`
|
|
72
|
+
- Linux `arm64`
|
|
73
|
+
- Linux `x64`
|
|
74
|
+
- Windows `arm64`
|
|
75
|
+
- Windows `x64`
|
|
76
|
+
|
|
77
|
+
For local execution from an unpublished checkout, use:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npm install
|
|
81
|
+
npm run cli -- configure claude
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If you want the install step to also update your client configuration
|
|
85
|
+
immediately:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
KLOVIS_CLIENT=claude npm install
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Supported values are `codex`, `claude`, and `both`.
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
Configure one or both clients to point to Klovys99:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npx klovys99 configure codex
|
|
99
|
+
npx klovys99 configure claude
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The historical command name still works:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
npx klovis configure claude
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
From a local checkout that is not published to npm, prefer:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npm run cli -- configure claude
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Or configure both at once:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
npx klovys99 configure both
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then start the proxy:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
npx klovys99 start
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
By default, Klovys99 listens on `http://127.0.0.1:8080` and exposes these local
|
|
127
|
+
routes:
|
|
128
|
+
|
|
129
|
+
- `http://127.0.0.1:8080/anthropic` for Claude Code and other Anthropic clients
|
|
130
|
+
- `http://127.0.0.1:8080/openai/v1` for Codex and other OpenAI-compatible
|
|
131
|
+
clients
|
|
132
|
+
|
|
133
|
+
The historical unprefixed route still exists and forwards to
|
|
134
|
+
`KLOVIS_TARGET_URL`, which defaults to `https://api.anthropic.com`.
|
|
135
|
+
|
|
136
|
+
## Client Configuration
|
|
137
|
+
|
|
138
|
+
### Codex
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
npx klovys99 configure codex
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This updates `~/.codex/config.toml` and sets:
|
|
145
|
+
|
|
146
|
+
```toml
|
|
147
|
+
openai_base_url = "http://127.0.0.1:8080/openai/v1"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Claude Code
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
npx klovys99 configure claude
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This updates `~/.claude/settings.json` and sets:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"env": {
|
|
161
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8080/anthropic"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
If you want another listen URL written into both clients, pass `--base-url`:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
npx klovys99 configure both --base-url http://127.0.0.1:9090
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Quick API Checks
|
|
173
|
+
|
|
174
|
+
Anthropic-style request through Klovys99:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
curl http://127.0.0.1:8080/anthropic/v1/messages \
|
|
178
|
+
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
|
179
|
+
-H "anthropic-version: 2023-06-01" \
|
|
180
|
+
-H "content-type: application/json" \
|
|
181
|
+
-d '{
|
|
182
|
+
"model": "claude-sonnet-4-5",
|
|
183
|
+
"max_tokens": 128,
|
|
184
|
+
"messages": [
|
|
185
|
+
{
|
|
186
|
+
"role": "user",
|
|
187
|
+
"content": "Email Alice at alice@example.com"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
OpenAI Responses-style request through Klovys99:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
curl http://127.0.0.1:8080/openai/v1/responses \
|
|
197
|
+
-H "authorization: Bearer $OPENAI_API_KEY" \
|
|
198
|
+
-H "content-type: application/json" \
|
|
199
|
+
-d '{
|
|
200
|
+
"model": "gpt-5",
|
|
201
|
+
"input": "Email Alice at alice@example.com"
|
|
202
|
+
}'
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Upstream providers receive the same request shape, with sensitive values
|
|
206
|
+
replaced by pseudonym tokens such as `[EMAIL_1]`.
|
|
207
|
+
|
|
208
|
+
## How It Works
|
|
209
|
+
|
|
210
|
+
Klovys99 reads each incoming JSON request body, anonymizes supported prompt
|
|
211
|
+
content, then forwards the modified request to the configured upstream.
|
|
212
|
+
|
|
213
|
+
The proxy anonymizes:
|
|
214
|
+
|
|
215
|
+
- every `<session>...</session>` block found anywhere in a JSON request body;
|
|
216
|
+
- text content in prompts, system messages, `<system-reminder>` blocks, text
|
|
217
|
+
file context, and tool results;
|
|
218
|
+
- text document sources where `source.type` is `text`.
|
|
219
|
+
|
|
220
|
+
Structural metadata such as model names, roles, content block types, tool IDs,
|
|
221
|
+
tool names, media types, cache-control values, and base64 document data is left
|
|
222
|
+
unchanged so the upstream request shape remains valid.
|
|
223
|
+
|
|
224
|
+
For a single proxy process, repeated values are mapped to stable tokens. For
|
|
225
|
+
example, the same email address is replaced by the same `[EMAIL_N]` token across
|
|
226
|
+
requests handled by that process.
|
|
227
|
+
|
|
228
|
+
When matches overlap, the detector with the highest priority wins. If priorities
|
|
229
|
+
are equal, the longest match wins.
|
|
230
|
+
|
|
231
|
+
## Configuration
|
|
232
|
+
|
|
233
|
+
Klovys99 runtime is configured with environment variables.
|
|
234
|
+
|
|
235
|
+
| Variable | Default | Description |
|
|
236
|
+
| --- | --- | --- |
|
|
237
|
+
| `KLOVIS_ADDR` | `127.0.0.1:8080` | Listen address for the local proxy. |
|
|
238
|
+
| `KLOVIS_TARGET_URL` | `https://api.anthropic.com` | Upstream used by legacy unprefixed routes such as `/v1/messages`. |
|
|
239
|
+
| `KLOVIS_ANTHROPIC_TARGET_URL` | `https://api.anthropic.com` | Upstream used by `/anthropic/...` routes. |
|
|
240
|
+
| `KLOVIS_OPENAI_TARGET_URL` | `https://api.openai.com` | Upstream used by `/openai/...` routes. |
|
|
241
|
+
| `KLOVIS_PROXY_DEBUG` | `false` | Enables debug traffic body logging when set to `true`. |
|
|
242
|
+
| `KLOVIS_LOG_TO_FILE` | `false` | Writes logs to `proxy.log` instead of stdout when set to `true`. |
|
|
243
|
+
| `KLOVIS_LLM_ENABLED` | `false` | Enables optional local LLM extraction through Ollama. |
|
|
244
|
+
| `KLOVIS_LLM_URL` | `http://localhost:11434` | Ollama base URL. |
|
|
245
|
+
| `KLOVIS_LLM_MODEL` | `mistral` | Ollama model used for entity extraction. |
|
|
246
|
+
| `KLOVIS_LLM_TIMEOUT` | `30s` | Startup and request timeout for LLM calls. |
|
|
247
|
+
| `KLOVIS_LLM_MAX_CHARS` | `1000` | Maximum input bytes sent to the LLM per chunk. |
|
|
248
|
+
| `KLOVIS_LLM_AUTOSTART` | `false` | Starts `ollama serve` automatically when the Ollama URL is local and not already reachable. |
|
|
249
|
+
|
|
250
|
+
The npm wrapper also honors:
|
|
251
|
+
|
|
252
|
+
| Variable | Description |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| `KLOVIS_CLIENT` | Client to configure during `npm install`: `codex`, `claude`, or `both`. |
|
|
255
|
+
| `KLOVIS_BASE_URL` | Base URL written by `klovys99 configure` or `npm install` auto-configuration. |
|
|
256
|
+
| `KLOVIS_SKIP_DOWNLOAD` | Skips the prebuilt binary download during `postinstall` when set to `true`. |
|
|
257
|
+
| `KLOVIS_SKIP_BUILD` | Skips the local Go build fallback during `postinstall` when set to `true`. |
|
|
258
|
+
| `KLOVIS_SKIP_CONFIGURE` | Skips client configuration during `postinstall` when set to `true`. |
|
|
259
|
+
|
|
260
|
+
Boolean variables accept only `true` or `false`.
|
|
261
|
+
|
|
262
|
+
## Logs
|
|
263
|
+
|
|
264
|
+
Klovys99 writes structured application logs to stdout by default. To write logs to
|
|
265
|
+
`proxy.log` instead, enable file logging:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
KLOVIS_LOG_TO_FILE=true npx klovys99 start
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
To inspect request bodies before and after anonymization, enable debug logging:
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
KLOVIS_LOG_TO_FILE=true KLOVIS_PROXY_DEBUG=true npx klovys99 start
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Use debug mode carefully, because it records both the original incoming request
|
|
278
|
+
body and the anonymized upstream request body in whichever log destination is
|
|
279
|
+
configured.
|
|
280
|
+
|
|
281
|
+
## Optional LLM Extraction
|
|
282
|
+
|
|
283
|
+
LLM extraction is disabled by default. Enable it with:
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
KLOVIS_LLM_ENABLED=true npx klovys99 start
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
When enabled, Klovys99 checks the Ollama connection during startup and runs a small
|
|
290
|
+
extraction probe before accepting traffic. If startup verification fails, the
|
|
291
|
+
proxy exits.
|
|
292
|
+
|
|
293
|
+
By default, Klovys99 does not start Ollama for you. Start Ollama separately before
|
|
294
|
+
enabling LLM extraction, or opt in to local autostart:
|
|
295
|
+
|
|
296
|
+
```sh
|
|
297
|
+
KLOVIS_LLM_ENABLED=true KLOVIS_LLM_AUTOSTART=true npx klovys99 start
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Autostart only applies to local Ollama URLs such as `http://localhost:11434` or
|
|
301
|
+
loopback IP addresses. Remote Ollama URLs are never started by Klovys99.
|
|
302
|
+
|
|
303
|
+
Deterministic detectors remain the baseline. LLM matches are added when
|
|
304
|
+
available and have lower priority than deterministic regex, Gitleaks, and
|
|
305
|
+
Presidio matches. If the LLM fails during a request, Klovys99 logs the technical
|
|
306
|
+
error and continues with deterministic anonymization.
|
|
307
|
+
|
|
308
|
+
## Detectors
|
|
309
|
+
|
|
310
|
+
Klovys99 combines built-in detectors with external rules loaded at startup.
|
|
311
|
+
External rule payloads are cached for 24 hours in the user cache directory under
|
|
312
|
+
`klovys99/external-rules`.
|
|
313
|
+
|
|
314
|
+
| Category | Source | Priority | Description |
|
|
315
|
+
| --- | --- | ---: | --- |
|
|
316
|
+
| `EMAIL` | Built-in / Presidio | 1000 / 600 | Email addresses, normalized in lowercase for stable tokens. |
|
|
317
|
+
| `NIR` | Built-in | 1000 | French social security numbers, including spaced formats and Corsica departments `2A` and `2B`. |
|
|
318
|
+
| `IBAN` | Built-in / Presidio | 1000 / 600 | IBAN-like account identifiers, normalized by removing separators. |
|
|
319
|
+
| `IP` | Built-in / Presidio | 900 / 600 | IPv4 and IPv6 addresses. |
|
|
320
|
+
| `CREDIT_CARD` | Built-in / Presidio | 900 / 600 | Credit card-like digit sequences. |
|
|
321
|
+
| `MAC_ADDRESS` | Built-in / Presidio | 900 / 600 | MAC addresses with `:` or `-` separators. |
|
|
322
|
+
| `PHONE` | Built-in | 700 | French and common international phone numbers. |
|
|
323
|
+
| `DATE` | Built-in / Presidio / LLM | 600 / external / 50 | Conservatively labelled birth dates and supported contextual dates. |
|
|
324
|
+
| `BLOOD_TYPE` | Built-in | 600 | Contextual blood groups such as `Groupe sanguin O+`. |
|
|
325
|
+
| `SECRET` | Gitleaks | 600 | Secrets loaded dynamically from the official Gitleaks config. |
|
|
326
|
+
| `CRYPTO` | Presidio | 600 | Cryptocurrency wallet identifiers loaded from supported Presidio recognizers. |
|
|
327
|
+
| `ADDRESS` | Built-in / LLM | 900 / 700 / 50 | French postal addresses, labelled addresses, and optional contextual LLM matches. |
|
|
328
|
+
| `NAME` | Built-in | 900 | Contextual names following strong French or English cues and form labels. |
|
|
329
|
+
| `FIRST_NAME` | Built-in | 500 | Conservatively labelled first names. |
|
|
330
|
+
| `LAST_NAME` | Built-in | 500 | Conservatively labelled last names. |
|
|
331
|
+
| `NUMERIC_ID` | Built-in | 100 | Generic long numeric IDs. |
|
|
332
|
+
| `REFERENCE_ID` | Built-in | 100 | Labelled alphanumeric references requiring letters and digits. |
|
|
333
|
+
| `PERSON_NAME` | LLM | 50 | Contextual full names found by the local model. |
|
|
334
|
+
| `DATE` | LLM / Presidio | 50 / 600 | Dates tied to identity, family, documents, health, work, or events. |
|
|
335
|
+
| `VEHICLE_PLATE` | LLM | 50 | Vehicle registration plates found by the local model. |
|
|
336
|
+
|
|
337
|
+
## Claude Code Notes
|
|
338
|
+
|
|
339
|
+
When Claude Code uses a non-first-party `ANTHROPIC_BASE_URL`, some Claude
|
|
340
|
+
features behave differently upstream. In practice:
|
|
341
|
+
|
|
342
|
+
- Remote Control is disabled by Claude Code when the base URL does not point to
|
|
343
|
+
`api.anthropic.com`.
|
|
344
|
+
- Tool search behavior changes when routing through a proxy. If you need
|
|
345
|
+
deferred tool references, set `ENABLE_TOOL_SEARCH=true` in your Claude
|
|
346
|
+
environment because Klovys99 forwards `tool_reference` blocks unchanged.
|
|
347
|
+
|
|
348
|
+
## Development
|
|
349
|
+
|
|
350
|
+
Clone the repository and install both Node and Go dependencies:
|
|
351
|
+
|
|
352
|
+
```sh
|
|
353
|
+
git clone https://github.com/Korbicorp/klovys99.git
|
|
354
|
+
cd klovys99
|
|
355
|
+
npm install
|
|
356
|
+
go mod download
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Tagged releases build one binary per supported OS and architecture in GitHub
|
|
360
|
+
Actions. If `NPM_TOKEN_KLOVYS` is configured in repository secrets, the same tag
|
|
361
|
+
workflow also publishes the npm package after uploading the release assets.
|
|
362
|
+
|
|
363
|
+
Run the test suites:
|
|
364
|
+
|
|
365
|
+
```sh
|
|
366
|
+
go test ./...
|
|
367
|
+
node --test npm/test/*.test.js
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Run the proxy locally without npm:
|
|
371
|
+
|
|
372
|
+
```sh
|
|
373
|
+
go run ./cmd/klovys99
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Format Go code before submitting changes:
|
|
377
|
+
|
|
378
|
+
```sh
|
|
379
|
+
gofmt -w ./cmd ./internal
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Security Notes
|
|
383
|
+
|
|
384
|
+
Klovys99 reduces the amount of sensitive data sent upstream, but it is not a
|
|
385
|
+
formal data-loss-prevention guarantee. Review detector coverage for your own
|
|
386
|
+
threat model before using it with production data.
|
|
387
|
+
|
|
388
|
+
External Gitleaks and Presidio rules are loaded from their upstream repositories
|
|
389
|
+
by default. Cached copies are reused for 24 hours and stale cache entries may be
|
|
390
|
+
used as a fallback if a refresh fails.
|
|
391
|
+
|
|
392
|
+
## Contributing
|
|
393
|
+
|
|
394
|
+
Issues and pull requests are welcome. For code changes, please include focused
|
|
395
|
+
tests that cover the behavior being changed.
|
|
396
|
+
|
|
397
|
+
Useful checks before opening a pull request:
|
|
398
|
+
|
|
399
|
+
```sh
|
|
400
|
+
go test ./...
|
|
401
|
+
node --test npm/test/*.test.js
|
|
402
|
+
gofmt -w ./cmd ./internal
|
|
403
|
+
```
|
package/npm/cli.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const {
|
|
8
|
+
DEFAULT_BASE_URL,
|
|
9
|
+
configureClients,
|
|
10
|
+
normalizeBaseUrl,
|
|
11
|
+
} = require("./lib/configure");
|
|
12
|
+
|
|
13
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
14
|
+
|
|
15
|
+
function main(argv) {
|
|
16
|
+
const [command = "start", ...rest] = argv;
|
|
17
|
+
switch (command) {
|
|
18
|
+
case "configure":
|
|
19
|
+
return runConfigure(rest);
|
|
20
|
+
case "start":
|
|
21
|
+
case "serve":
|
|
22
|
+
return runBinary(rest);
|
|
23
|
+
case "help":
|
|
24
|
+
case "--help":
|
|
25
|
+
case "-h":
|
|
26
|
+
printHelp();
|
|
27
|
+
return 0;
|
|
28
|
+
default:
|
|
29
|
+
return runBinary([command, ...rest]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runConfigure(args) {
|
|
34
|
+
const options = parseConfigureArgs(args);
|
|
35
|
+
const results = configureClients(options);
|
|
36
|
+
for (const result of results) {
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
`Configured ${result.client} to use ${result.baseUrl} via ${result.path}\n`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseConfigureArgs(args) {
|
|
45
|
+
let client = process.env.KLOVIS_CLIENT || "";
|
|
46
|
+
let baseUrl = process.env.KLOVIS_BASE_URL || DEFAULT_BASE_URL;
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
49
|
+
const value = args[index];
|
|
50
|
+
if (value === "--base-url") {
|
|
51
|
+
const next = args[index + 1];
|
|
52
|
+
if (!next) {
|
|
53
|
+
throw new Error("missing value for --base-url");
|
|
54
|
+
}
|
|
55
|
+
baseUrl = next;
|
|
56
|
+
index += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (value.startsWith("--base-url=")) {
|
|
60
|
+
baseUrl = value.slice("--base-url=".length);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!client) {
|
|
64
|
+
client = value;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`unexpected argument ${JSON.stringify(value)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!client) {
|
|
71
|
+
throw new Error("missing client, expected codex, claude, or both");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
client,
|
|
76
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runBinary(args) {
|
|
81
|
+
const binaryPath = resolveBinaryPath();
|
|
82
|
+
const result = spawnSync(binaryPath, args, {
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
stdio: "inherit",
|
|
85
|
+
env: process.env,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (result.error) {
|
|
89
|
+
throw result.error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result.status || 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveBinaryPath() {
|
|
96
|
+
const binaryName = process.platform === "win32" ? "klovys99.exe" : "klovys99";
|
|
97
|
+
const binaryPath = path.join(packageRoot, "dist", binaryName);
|
|
98
|
+
if (!fs.existsSync(binaryPath)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`missing installed binary at ${binaryPath}. Run npm install again. ` +
|
|
101
|
+
`From a source checkout, you can also build with go build -o dist/${binaryName} ./cmd/klovys99.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return binaryPath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printHelp() {
|
|
108
|
+
process.stdout.write(`Klovys99
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
klovys99 start
|
|
112
|
+
klovys99 configure codex [--base-url http://127.0.0.1:8080]
|
|
113
|
+
klovys99 configure claude [--base-url http://127.0.0.1:8080]
|
|
114
|
+
klovys99 configure both [--base-url http://127.0.0.1:8080]
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
process.exitCode = main(process.argv.slice(2));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
process.stderr.write(`${error.message}\n`);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BASE_URL = "http://127.0.0.1:8080";
|
|
8
|
+
const KLOVIS_BEGIN_MARKER = "# BEGIN KLOVIS";
|
|
9
|
+
const KLOVIS_END_MARKER = "# END KLOVIS";
|
|
10
|
+
|
|
11
|
+
function normalizeBaseUrl(baseUrl = DEFAULT_BASE_URL) {
|
|
12
|
+
const trimmed = String(baseUrl).trim().replace(/\/+$/, "");
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = new URL(trimmed);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(`invalid base URL ${JSON.stringify(baseUrl)}: ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (!parsed.protocol || !parsed.host) {
|
|
20
|
+
throw new Error(`invalid base URL ${JSON.stringify(baseUrl)}: missing protocol or host`);
|
|
21
|
+
}
|
|
22
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function codexProxyBaseUrl(baseUrl) {
|
|
26
|
+
return `${normalizeBaseUrl(baseUrl)}/openai/v1`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function claudeProxyBaseUrl(baseUrl) {
|
|
30
|
+
return `${normalizeBaseUrl(baseUrl)}/anthropic`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function configureClients({ client, baseUrl, homeDir = os.homedir() }) {
|
|
34
|
+
switch (client) {
|
|
35
|
+
case "codex":
|
|
36
|
+
return [configureCodex({ baseUrl, homeDir })];
|
|
37
|
+
case "claude":
|
|
38
|
+
return [configureClaude({ baseUrl, homeDir })];
|
|
39
|
+
case "both":
|
|
40
|
+
return [
|
|
41
|
+
configureCodex({ baseUrl, homeDir }),
|
|
42
|
+
configureClaude({ baseUrl, homeDir }),
|
|
43
|
+
];
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`unsupported client ${JSON.stringify(client)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function configureCodex({ baseUrl, homeDir = os.homedir() }) {
|
|
50
|
+
const configPath = path.join(homeDir, ".codex", "config.toml");
|
|
51
|
+
const desiredBaseUrl = codexProxyBaseUrl(baseUrl);
|
|
52
|
+
const existing = readFileIfExists(configPath);
|
|
53
|
+
const updated = updateCodexConfig(existing, desiredBaseUrl);
|
|
54
|
+
writeTextFile(configPath, updated);
|
|
55
|
+
return {
|
|
56
|
+
client: "codex",
|
|
57
|
+
path: configPath,
|
|
58
|
+
baseUrl: desiredBaseUrl,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function configureClaude({ baseUrl, homeDir = os.homedir() }) {
|
|
63
|
+
const configPath = path.join(homeDir, ".claude", "settings.json");
|
|
64
|
+
const desiredBaseUrl = claudeProxyBaseUrl(baseUrl);
|
|
65
|
+
const existing = readFileIfExists(configPath);
|
|
66
|
+
const updated = updateClaudeConfig(existing, desiredBaseUrl);
|
|
67
|
+
writeTextFile(configPath, `${JSON.stringify(updated, null, 2)}\n`);
|
|
68
|
+
return {
|
|
69
|
+
client: "claude",
|
|
70
|
+
path: configPath,
|
|
71
|
+
baseUrl: desiredBaseUrl,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function updateCodexConfig(content, desiredBaseUrl) {
|
|
76
|
+
const managedBlock = [
|
|
77
|
+
KLOVIS_BEGIN_MARKER,
|
|
78
|
+
`openai_base_url = ${tomlString(desiredBaseUrl)}`,
|
|
79
|
+
KLOVIS_END_MARKER,
|
|
80
|
+
].join("\n");
|
|
81
|
+
|
|
82
|
+
if (!content.trim()) {
|
|
83
|
+
return `${managedBlock}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const managedBlockPattern = new RegExp(
|
|
87
|
+
`${escapeRegExp(KLOVIS_BEGIN_MARKER)}[\\s\\S]*?${escapeRegExp(KLOVIS_END_MARKER)}`,
|
|
88
|
+
"m",
|
|
89
|
+
);
|
|
90
|
+
if (managedBlockPattern.test(content)) {
|
|
91
|
+
return ensureTrailingNewline(content.replace(managedBlockPattern, managedBlock));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const openAIBaseURLPattern = /^openai_base_url\s*=.*$/m;
|
|
95
|
+
if (openAIBaseURLPattern.test(content)) {
|
|
96
|
+
return ensureTrailingNewline(
|
|
97
|
+
content.replace(openAIBaseURLPattern, `openai_base_url = ${tomlString(desiredBaseUrl)}`),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const trimmed = content.replace(/\s+$/, "");
|
|
102
|
+
return `${trimmed}\n\n${managedBlock}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateClaudeConfig(content, desiredBaseUrl) {
|
|
106
|
+
let parsed;
|
|
107
|
+
if (content.trim() === "") {
|
|
108
|
+
parsed = {};
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(content);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new Error(`invalid Claude settings JSON: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
118
|
+
throw new Error("invalid Claude settings JSON: top-level value must be an object");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const env = parsed.env;
|
|
122
|
+
if (env !== undefined && (env === null || Array.isArray(env) || typeof env !== "object")) {
|
|
123
|
+
throw new Error("invalid Claude settings JSON: env must be an object");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
parsed.env = { ...(env || {}), ANTHROPIC_BASE_URL: desiredBaseUrl };
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readFileIfExists(filePath) {
|
|
131
|
+
if (!fs.existsSync(filePath)) {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
return fs.readFileSync(filePath, "utf8");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeTextFile(filePath, content) {
|
|
138
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
139
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function tomlString(value) {
|
|
143
|
+
return JSON.stringify(String(value));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ensureTrailingNewline(content) {
|
|
147
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function escapeRegExp(value) {
|
|
151
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
DEFAULT_BASE_URL,
|
|
156
|
+
claudeProxyBaseUrl,
|
|
157
|
+
codexProxyBaseUrl,
|
|
158
|
+
configureClaude,
|
|
159
|
+
configureClients,
|
|
160
|
+
configureCodex,
|
|
161
|
+
normalizeBaseUrl,
|
|
162
|
+
updateClaudeConfig,
|
|
163
|
+
updateCodexConfig,
|
|
164
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const https = require("node:https");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { pipeline } = require("node:stream/promises");
|
|
7
|
+
|
|
8
|
+
const RELEASE_HOST = "https://github.com";
|
|
9
|
+
|
|
10
|
+
function detectTarget(platform = process.platform, arch = process.arch) {
|
|
11
|
+
const key = `${platform}/${arch}`;
|
|
12
|
+
switch (key) {
|
|
13
|
+
case "darwin/arm64":
|
|
14
|
+
return { os: "darwin", arch: "arm64", extension: "" };
|
|
15
|
+
case "darwin/x64":
|
|
16
|
+
return { os: "darwin", arch: "amd64", extension: "" };
|
|
17
|
+
case "linux/arm64":
|
|
18
|
+
return { os: "linux", arch: "arm64", extension: "" };
|
|
19
|
+
case "linux/x64":
|
|
20
|
+
return { os: "linux", arch: "amd64", extension: "" };
|
|
21
|
+
case "win32/arm64":
|
|
22
|
+
return { os: "windows", arch: "arm64", extension: ".exe" };
|
|
23
|
+
case "win32/x64":
|
|
24
|
+
return { os: "windows", arch: "amd64", extension: ".exe" };
|
|
25
|
+
default:
|
|
26
|
+
throw new Error(
|
|
27
|
+
`unsupported platform ${JSON.stringify(platform)} and architecture ${JSON.stringify(arch)}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function releaseTag(version) {
|
|
33
|
+
const trimmed = String(version || "").trim();
|
|
34
|
+
if (!trimmed) {
|
|
35
|
+
throw new Error("missing package version");
|
|
36
|
+
}
|
|
37
|
+
return trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizedVersion(version) {
|
|
41
|
+
return releaseTag(version).slice(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function binaryFileName(platform = process.platform) {
|
|
45
|
+
return platform === "win32" ? "klovys99.exe" : "klovys99";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function releaseAssetName(version, target) {
|
|
49
|
+
return `klovys99_${normalizedVersion(version)}_${target.os}_${target.arch}${target.extension}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function releaseAssetUrl(version, target, repository = defaultRepository()) {
|
|
53
|
+
const tag = releaseTag(version);
|
|
54
|
+
const assetName = releaseAssetName(version, target);
|
|
55
|
+
return `${RELEASE_HOST}/${repository.owner}/${repository.name}/releases/download/${tag}/${assetName}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readPackageManifest(packageRoot) {
|
|
59
|
+
const manifestPath = path.join(packageRoot, "package.json");
|
|
60
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readPackageVersion(packageRoot) {
|
|
64
|
+
return readPackageManifest(packageRoot).version;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readRepository(packageRoot) {
|
|
68
|
+
const manifest = readPackageManifest(packageRoot);
|
|
69
|
+
const value =
|
|
70
|
+
typeof manifest.repository === "string"
|
|
71
|
+
? manifest.repository
|
|
72
|
+
: manifest.repository && typeof manifest.repository.url === "string"
|
|
73
|
+
? manifest.repository.url
|
|
74
|
+
: "";
|
|
75
|
+
return parseRepository(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultRepository() {
|
|
79
|
+
return {
|
|
80
|
+
owner: "Korbicorp",
|
|
81
|
+
name: "klovys99",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseRepository(value) {
|
|
86
|
+
const trimmed = String(value || "").trim();
|
|
87
|
+
if (!trimmed) {
|
|
88
|
+
return defaultRepository();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const normalized = trimmed
|
|
92
|
+
.replace(/^git\+/, "")
|
|
93
|
+
.replace(/^git@github\.com:/, "https://github.com/")
|
|
94
|
+
.replace(/\.git$/, "");
|
|
95
|
+
const match = normalized.match(/github\.com\/([^/]+)\/([^/]+)$/);
|
|
96
|
+
if (!match) {
|
|
97
|
+
return defaultRepository();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
owner: match[1],
|
|
102
|
+
name: match[2],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function installReleaseBinary({
|
|
107
|
+
version,
|
|
108
|
+
packageRoot,
|
|
109
|
+
platform = process.platform,
|
|
110
|
+
arch = process.arch,
|
|
111
|
+
}) {
|
|
112
|
+
const target = detectTarget(platform, arch);
|
|
113
|
+
const binaryPath = path.join(packageRoot, "dist", binaryFileName(platform));
|
|
114
|
+
const assetUrl = releaseAssetUrl(version, target, readRepository(packageRoot));
|
|
115
|
+
|
|
116
|
+
await fs.promises.mkdir(path.dirname(binaryPath), { recursive: true });
|
|
117
|
+
await downloadToFile(assetUrl, binaryPath);
|
|
118
|
+
|
|
119
|
+
if (platform !== "win32") {
|
|
120
|
+
await fs.promises.chmod(binaryPath, 0o755);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
assetUrl,
|
|
125
|
+
binaryPath,
|
|
126
|
+
target,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function downloadToFile(url, destinationPath) {
|
|
131
|
+
const tempPath = `${destinationPath}.tmp`;
|
|
132
|
+
try {
|
|
133
|
+
await downloadWithRedirects(url, tempPath, 5);
|
|
134
|
+
await fs.promises.rename(tempPath, destinationPath);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function downloadWithRedirects(url, destinationPath, redirectsRemaining) {
|
|
142
|
+
const response = await request(url);
|
|
143
|
+
|
|
144
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
145
|
+
if (redirectsRemaining <= 0) {
|
|
146
|
+
response.resume();
|
|
147
|
+
throw new Error(`too many redirects while downloading ${url}`);
|
|
148
|
+
}
|
|
149
|
+
const redirectedUrl = new URL(response.headers.location, url).toString();
|
|
150
|
+
response.resume();
|
|
151
|
+
return downloadWithRedirects(redirectedUrl, destinationPath, redirectsRemaining - 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (response.statusCode !== 200) {
|
|
155
|
+
const body = await readResponseBody(response);
|
|
156
|
+
throw new Error(
|
|
157
|
+
`download ${url} failed with status ${response.statusCode}${body ? `: ${body}` : ""}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const file = fs.createWriteStream(destinationPath, { mode: 0o755 });
|
|
162
|
+
await pipeline(response, file);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function request(url) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const req = https.get(
|
|
168
|
+
url,
|
|
169
|
+
{
|
|
170
|
+
headers: {
|
|
171
|
+
"user-agent": "klovys99-installer",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
resolve,
|
|
175
|
+
);
|
|
176
|
+
req.on("error", reject);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readResponseBody(response) {
|
|
181
|
+
let body = "";
|
|
182
|
+
response.setEncoding("utf8");
|
|
183
|
+
for await (const chunk of response) {
|
|
184
|
+
body += chunk;
|
|
185
|
+
if (body.length > 512) {
|
|
186
|
+
body = `${body.slice(0, 512)}...`;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return body.trim();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
binaryFileName,
|
|
195
|
+
defaultRepository,
|
|
196
|
+
detectTarget,
|
|
197
|
+
installReleaseBinary,
|
|
198
|
+
normalizedVersion,
|
|
199
|
+
parseRepository,
|
|
200
|
+
readPackageVersion,
|
|
201
|
+
readRepository,
|
|
202
|
+
releaseAssetName,
|
|
203
|
+
releaseAssetUrl,
|
|
204
|
+
releaseTag,
|
|
205
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const { configureClients, normalizeBaseUrl } = require("./lib/configure");
|
|
8
|
+
const {
|
|
9
|
+
binaryFileName,
|
|
10
|
+
installReleaseBinary,
|
|
11
|
+
readPackageVersion,
|
|
12
|
+
} = require("./lib/install");
|
|
13
|
+
|
|
14
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
await ensureBinaryInstalled();
|
|
18
|
+
|
|
19
|
+
const client = process.env.KLOVIS_CLIENT || process.env.KLOVIS_SETUP_CLIENT;
|
|
20
|
+
if (!client || process.env.KLOVIS_SKIP_CONFIGURE === "true") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const baseUrl = normalizeBaseUrl(process.env.KLOVIS_BASE_URL || "http://127.0.0.1:8080");
|
|
25
|
+
const results = configureClients({ client, baseUrl });
|
|
26
|
+
for (const result of results) {
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
`Configured ${result.client} to use ${result.baseUrl} via ${result.path}\n`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureBinaryInstalled() {
|
|
34
|
+
if (process.env.KLOVIS_SKIP_DOWNLOAD === "true") {
|
|
35
|
+
process.stdout.write("Skipping Klovys99 binary download because KLOVIS_SKIP_DOWNLOAD=true\n");
|
|
36
|
+
if (canBuildFromSource()) {
|
|
37
|
+
buildBinary();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
"KLOVIS_SKIP_DOWNLOAD=true but no local Go source checkout is available for fallback build",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const version = readPackageVersion(packageRoot);
|
|
46
|
+
try {
|
|
47
|
+
const result = await installReleaseBinary({ version, packageRoot });
|
|
48
|
+
process.stdout.write(`Installed Klovys99 binary from ${result.assetUrl}\n`);
|
|
49
|
+
return;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!canBuildFromSource()) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`unable to install Klovys99 prebuilt binary: ${error.message}. ` +
|
|
54
|
+
"This package expects a published GitHub release for the current version.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(
|
|
58
|
+
`Prebuilt Klovys99 binary download failed (${error.message}). Falling back to local Go build.\n`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
buildBinary();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function canBuildFromSource() {
|
|
66
|
+
return (
|
|
67
|
+
process.env.KLOVIS_SKIP_BUILD !== "true" &&
|
|
68
|
+
fs.existsSync(path.join(packageRoot, "cmd", "klovys99"))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildBinary() {
|
|
73
|
+
const binaryName = binaryFileName(process.platform);
|
|
74
|
+
const binaryPath = path.join(packageRoot, "dist", binaryName);
|
|
75
|
+
fs.mkdirSync(path.dirname(binaryPath), { recursive: true });
|
|
76
|
+
|
|
77
|
+
const result = spawnSync("go", ["build", "-o", binaryPath, "./cmd/klovys99"], {
|
|
78
|
+
cwd: packageRoot,
|
|
79
|
+
stdio: "inherit",
|
|
80
|
+
env: process.env,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (result.error) {
|
|
84
|
+
throw new Error(`unable to run go build: ${result.error.message}`);
|
|
85
|
+
}
|
|
86
|
+
if (result.status !== 0) {
|
|
87
|
+
throw new Error(`go build failed with exit code ${result.status}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
Promise.resolve(main()).catch((error) => {
|
|
93
|
+
process.stderr.write(`${error.message}\n`);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
process.stderr.write(`${error.message}\n`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "klovys99",
|
|
3
|
+
"version": "0.1.0-main.10",
|
|
4
|
+
"description": "Local prompt anonymizing proxy for Codex and Claude Code",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Korbicorp/klovys99.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Korbicorp/klovys99",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Korbicorp/klovys99/issues"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md",
|
|
17
|
+
"npm/cli.js",
|
|
18
|
+
"npm/postinstall.js",
|
|
19
|
+
"npm/lib/"
|
|
20
|
+
],
|
|
21
|
+
"bin": {
|
|
22
|
+
"klovis": "npm/cli.js",
|
|
23
|
+
"klovys99": "npm/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"cli": "node npm/cli.js",
|
|
27
|
+
"postinstall": "node npm/postinstall.js",
|
|
28
|
+
"test:node": "node --test npm/test/*.test.js"
|
|
29
|
+
}
|
|
30
|
+
}
|