anolisa-tokenless 0.7.11 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  Token-Less combines complementary strategies to minimize LLM token consumption:
8
8
 
9
9
  - **Schema & Response Compression** — Compresses OpenAI Function Calling tool definitions and API responses via the `tokenless-schema` library, cutting structural overhead before tokens ever reach the context window.
10
- - **TOON Context Compression** — Encodes JSON responses to TOON (Token-Oriented Object Notation) format via the `toon-format` library linked into `tokenless`, reducing token usage by 15-40% for structured data.
10
+ - **TOON Context Compression** — Encodes JSON responses to TOON (Token-Oriented Object Notation) format via the `toon-format` library linked into `tokenless`, reducing syntax overhead for suitable structured data.
11
11
  - **Command Rewriting** — Integrates [RTK](https://github.com/rtk-ai/rtk) to filter and rewrite CLI command output, eliminating noise that would otherwise waste 60–90% of tokens.
12
12
  - **Tool Ready (legacy, hard-disabled)** — Its pre-call dependency checks are retained in source but unconditionally bypassed while the readiness model is redesigned.
13
13
 
@@ -27,12 +27,12 @@ cover schema compression, RTK rewriting, response compression, TOON, retrieval,
27
27
 
28
28
  ## Features
29
29
 
30
- | Capability | Token Savings | Details |
30
+ | Capability | Savings indicator | Details |
31
31
  |---|---|---|
32
- | Schema compression | ~57% | Compresses OpenAI Function Calling tool schemas |
33
- | Response compression | ~26–78% | Compresses API / tool responses (varies by content type) |
32
+ | Schema compression | 47.3% on reference fixture | Compresses OpenAI Function Calling tool schemas |
33
+ | Response compression | 65.8% on reference fixture | Compresses API / tool responses |
34
34
  | Reversible compression (stash) | — | Dropped array items are stashed and retrievable via `<<tokenless:KEY>>` markers |
35
- | TOON context compression | 15–40% | Encodes JSON to TOON format for LLMs |
35
+ | TOON context compression | 17.0% on reference response | Encodes JSON to TOON format for LLMs |
36
36
  | Command rewriting | 60–90% | Filters CLI output via RTK (70+ commands supported) |
37
37
  | Tool Ready | reduces retry waste | Legacy pre-call check, auto-fix, and blocking; hard-disabled |
38
38
  | OpenClaw plugin | — | Command rewriting ✅, Response compression ✅, Schema compression ✅ |
@@ -46,17 +46,28 @@ cover schema compression, RTK rewriting, response compression, TOON, retrieval,
46
46
  | AgentScope framework integration | — | Schema ✅, RTK ✅, Response ✅, TOON ✅, Retrieval ✅ |
47
47
  | Zero runtime deps | — | Pure Rust, single static binary |
48
48
 
49
+ The schema, response, and TOON figures above are isolated Tokenless 0.7.11
50
+ results on the repository's committed reference fixtures; they are neither a
51
+ production range nor additive. Compression depends on payload size and shape,
52
+ removable fields, configured thresholds, and the share of tool data in the
53
+ session. Short or already compact payloads may save only a few percent or pass
54
+ through unchanged. See [Measuring Tokenless Savings](../../docs/user-guide/en/token-saving/tokenless/measuring-savings.md#run-the-repository-reference-workload)
55
+ for the exact inputs, command, full output, and limitations.
56
+
49
57
  ## Applicable Scenarios & Expected Effects
50
58
 
51
- tokenless only removes redundancy from **tool call responses** before they enter the LLM context; it does not touch model reasoning or conversation history. The payoff depends heavily on the share and shape of tool responses in the session.
59
+ tokenless optimizes the tool-related content it handles—tool schemas, tool/API
60
+ responses, and supported shell output—before it enters the LLM context. It does
61
+ not touch model reasoning or conversation history. The payoff depends heavily
62
+ on the share and shape of that content in the session.
52
63
 
53
64
  ### Where it pays off
54
65
 
55
66
  | Workload | Primary strategy | Why |
56
67
  |----------|-----------------|-----|
57
68
  | Shell-heavy (build/test/triage) | Command rewriting (RTK) | `cargo`/`npm`/`go`/`pytest` output carries lots of progress/warning noise; RTK cuts 60–90% |
58
- | API/fetch-heavy (REST, web_fetch) | Response compression + TOON | JSON carries debug/null/empty and syntax overhead; 26–78% compression, TOON adds 15–40% |
59
- | Agents with many tools | Schema compression | Many Function Calling definitions carry verbose descriptions; ~57% |
69
+ | API/fetch-heavy (REST, web_fetch) | Response compression + TOON | JSON may carry removable debug/null/empty fields; sufficiently large, regular structures also have reducible syntax overhead |
70
+ | Agents with many tools | Schema compression | Many Function Calling definitions carry verbose descriptions and removable metadata |
60
71
  | Long responses that must stay faithful | Reversible compression (Stash) | Truncated content is `retrieve`-able end-to-end lossless; thresholds can be tightened safely |
61
72
 
62
73
  ### Where it pays little or doesn't apply
@@ -264,6 +275,15 @@ Compress a batch of tools (JSON array):
264
275
  tokenless compress-schema -f tools.json --batch
265
276
  ```
266
277
 
278
+ A top-level request object with a `tools` array is also accepted without
279
+ `--batch`; OpenAI wrappers, Gemini `functionDeclarations` tool objects, and
280
+ bare Function Calling declarations are compressed while non-function tools and
281
+ fields outside `tools` are preserved:
282
+
283
+ ```bash
284
+ tokenless compress-schema -f request.json
285
+ ```
286
+
267
287
  ### compress-response
268
288
 
269
289
  Compress an API response:
@@ -276,8 +296,12 @@ tokenless compress-response -f response.json
276
296
  curl -s https://api.example.com/data | tokenless compress-response
277
297
  ```
278
298
 
279
- By default `compress-response` stashes dropped array items so they can be
280
- retrieved later (see [Reversible compression](docs/stash-reversible-compression.md)).
299
+ Long arrays are truncated to a head+tail window: the first
300
+ `--truncate-arrays-at` items (default 32) plus the last
301
+ `--array-tail-preserve` items (default 8), with a truncation marker in
302
+ between; pass `--array-tail-preserve 0` for head-only truncation. By default
303
+ `compress-response` stashes the dropped middle segment so it can be retrieved
304
+ later (see [Reversible compression](docs/stash-reversible-compression.md)).
281
305
  Pass `--no-stash` for lossy truncation, or `--stash-db <path>` to override the
282
306
  stash database (default `~/.tokenless/stash.db`).
283
307
 
@@ -292,7 +316,7 @@ tokenless retrieve c30ccf5ed1125e0ed871ba8e
292
316
 
293
317
  # Or paste the whole truncation line — the hash is extracted automatically.
294
318
  # (Use the FULL 24-hex hash from your output; the value below is shorthand.)
295
- tokenless retrieve "<... 195 items truncated, retrieve with <<tokenless:c30ccf5ed1125e0ed871ba8e>>"
319
+ tokenless retrieve "<... 160 items truncated, retrieve with <<tokenless:c30ccf5ed1125e0ed871ba8e>>"
296
320
  ```
297
321
 
298
322
  ### compress-toon / decompress-toon
@@ -312,10 +336,13 @@ echo 'name: Alice\nage: 30' | tokenless decompress-toon
312
336
 
313
337
  ### Inspect token savings
314
338
 
315
- Use `show` to print the complete stored before/after payload, or `diff` to
316
- explain the estimated token saving and highlight only changed lines:
339
+ Use `stats summary` for totals, `show` for the stored before/after payload, or
340
+ `diff` to explain the estimated token saving and highlight only changed lines:
317
341
 
318
342
  ```bash
343
+ tokenless stats summary
344
+ tokenless stats summary --limit 1000
345
+ tokenless stats summary --compare <baseline-session> <active-session>
319
346
  tokenless stats show 42
320
347
  tokenless stats diff 42
321
348
  tokenless stats diff --session <session-id>
@@ -323,10 +350,13 @@ tokenless stats diff --session <session-id> --tool-use-id <tool-use-id>
323
350
  tokenless stats diff 42 --json
324
351
  ```
325
352
 
326
- Session overviews contain metrics only. Record and tool-use reports include a
327
- unified content diff; consecutive active stages are linked only when their
328
- stored output/input content matches exactly, avoiding duplicate intermediate
329
- token counts. See [Measuring Tokenless Savings](../../docs/user-guide/en/token-saving/tokenless/measuring-savings.md)
353
+ `stats summary --limit` must be a positive integer; `--limit 0` is rejected at
354
+ parse time. `--compare` fails if either session has no records instead of
355
+ reporting 0% savings. Session overviews contain metrics only. Record and
356
+ tool-use reports include a unified content diff; consecutive active stages are
357
+ linked only when their stored output/input content matches exactly, avoiding
358
+ duplicate intermediate token counts. See
359
+ [Measuring Tokenless Savings](../../docs/user-guide/en/token-saving/tokenless/measuring-savings.md)
330
360
  for options and measurement limits.
331
361
 
332
362
  ### Database location
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Token-Less context compression for Claude Code — RTK command rewriting, response/TOON compression, and Tool Ready environment pre-check",
5
5
  "author": { "name": "ANOLISA" },
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Intelligent tool response compression, environment error detection, and token optimization. Strips noise (debug fields, nulls, empty values), truncates long strings/arrays, applies TOON encoding for JSON responses, and classifies environment errors with actionable fix hints.",
5
5
  "keywords": [
6
6
  "compression",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "contextFileName": "COPILOT.md",
5
5
  "hooks": {
6
6
  "PreToolUse": [
@@ -11,11 +11,19 @@ The agent ID is read from the TOKENLESS_AGENT_ID environment variable
11
11
  (set by the install action script).
12
12
  """
13
13
 
14
+ from __future__ import annotations
15
+
16
+ import contextlib
14
17
  import json
15
18
  import os
16
19
  import subprocess
17
20
  import sys
18
21
 
22
+ try: # POSIX hosts (cosh / Cosh-NG) — the platforms these hooks target.
23
+ import fcntl
24
+ except ImportError: # pragma: no cover - non-POSIX fallback keeps best-effort
25
+ fcntl = None
26
+
19
27
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
20
28
 
21
29
  from hook_utils import (
@@ -25,6 +33,7 @@ from hook_utils import (
25
33
  resolve_agent_id,
26
34
  resolve_binary,
27
35
  resolve_tool_call_id,
36
+ secure_write_text,
28
37
  skip,
29
38
  warn,
30
39
  )
@@ -33,6 +42,63 @@ from hook_utils import (
33
42
 
34
43
  _AGENT_ID = resolve_agent_id()
35
44
 
45
+ # One marker file holds the session keys that already emitted the "no tool
46
+ # declarations" warning — one key per line, most recent last — so the warning
47
+ # repeats at most once per session even though BeforeModel fires on every
48
+ # model turn. A single-value marker would not survive concurrent sessions
49
+ # under one HOME: session B's first warning would overwrite session A's key
50
+ # and make A warn again on its next turn. The list is bounded (oldest entries
51
+ # trimmed) so it cannot grow without limit; losing an evicted key only
52
+ # re-arms the warning for a long-idle session. All reads and writes of the
53
+ # marker run under the exclusive sidecar lock below: an unlocked
54
+ # read-modify-write lets two hook processes read the same old key set and
55
+ # write back over each other, silently losing one session's key.
56
+ _NO_TOOLS_WARN_MARKER = os.path.join(
57
+ os.path.expanduser("~"), ".tokenless", ".schema-hook-nowarn-session"
58
+ )
59
+
60
+ # Best-effort dedup bound: far above any plausible number of concurrently
61
+ # active sessions sharing one HOME.
62
+ _MAX_WARNED_SESSIONS = 64
63
+
64
+
65
+ @contextlib.contextmanager
66
+ def _marker_lock():
67
+ """Exclusive advisory lock guarding the marker read-modify-write.
68
+
69
+ Two hook processes warning for different sessions at the same time must
70
+ not read the same old key set and write back over each other. The lock
71
+ lives on a dedicated sidecar file that the marker write never replaces,
72
+ so the locked inode cannot be swapped out mid-section, and the kernel
73
+ drops an ``flock`` when its holder exits or crashes, so a dead hook
74
+ process cannot wedge later sessions. On platforms without ``fcntl``, or
75
+ when the lock file cannot be created, this degrades to the historical
76
+ unlocked best-effort behaviour instead of failing the warning.
77
+ """
78
+ if fcntl is None:
79
+ yield
80
+ return
81
+ lock_path = _NO_TOOLS_WARN_MARKER + ".lock"
82
+ fd = -1
83
+ try:
84
+ os.makedirs(os.path.dirname(lock_path), mode=0o700, exist_ok=True)
85
+ if os.path.islink(lock_path):
86
+ os.unlink(lock_path)
87
+ flags = os.O_RDWR | os.O_CREAT
88
+ if hasattr(os, "O_NOFOLLOW"):
89
+ flags |= os.O_NOFOLLOW
90
+ fd = os.open(lock_path, flags, 0o600)
91
+ fcntl.flock(fd, fcntl.LOCK_EX)
92
+ except OSError:
93
+ if fd >= 0:
94
+ os.close(fd)
95
+ yield # lock unavailable — keep the unlocked best-effort behaviour
96
+ return
97
+ try:
98
+ yield
99
+ finally:
100
+ os.close(fd)
101
+
36
102
 
37
103
  # -- helpers -----------------------------------------------------------------
38
104
 
@@ -45,6 +111,100 @@ def _is_json_array(data: str) -> bool:
45
111
  return False
46
112
 
47
113
 
114
+ def _session_warn_key(session_id: str) -> str:
115
+ """Normalize the session ID into the dedup key for the no-tools warning.
116
+
117
+ Hosts that omit a session ID share one key, so the warning still appears
118
+ at most once for them instead of on every turn.
119
+ """
120
+ key = (session_id or "").strip()
121
+ return key or "<no-session>"
122
+
123
+
124
+ def _read_warned_sessions() -> list:
125
+ """Read the warned session keys (one per line) from the marker file.
126
+
127
+ Tolerates a missing or corrupt marker — dedup is best-effort and a miss
128
+ simply means the session warns once (again).
129
+ """
130
+ try:
131
+ with open(_NO_TOOLS_WARN_MARKER, "r", encoding="utf-8") as handle:
132
+ return [line.strip() for line in handle if line.strip()]
133
+ except OSError:
134
+ return [] # first invocation, or unreadable marker
135
+ except UnicodeDecodeError:
136
+ return [] # corrupt marker — rebuild it on the next warning
137
+
138
+
139
+ def _should_warn_no_tools(session_id: str) -> bool:
140
+ """Return True if the no-tools warning has not been emitted for this
141
+ session yet, and record that it is being emitted now.
142
+
143
+ Every warned session key stays in the marker (bounded to the newest
144
+ ``_MAX_WARNED_SESSIONS`` entries), so concurrent sessions under one HOME
145
+ do not invalidate each other's dedup state. The check-and-record runs
146
+ inside ``_marker_lock`` so two first warnings racing for different
147
+ sessions serialize instead of overwriting each other's keys. Best-effort
148
+ by design: the marker lives under ``~/.tokenless`` and is read/written
149
+ with the same hardened helpers as other hook state, but any failure
150
+ falls back to warning — observability beats silence, and the warning
151
+ itself never affects the pass-through behaviour.
152
+ """
153
+ key = _session_warn_key(session_id)
154
+ with _marker_lock():
155
+ warned = _read_warned_sessions()
156
+ if key in warned:
157
+ return False
158
+ warned.append(key)
159
+ warned = warned[-_MAX_WARNED_SESSIONS:]
160
+ try:
161
+ secure_write_text(
162
+ _NO_TOOLS_WARN_MARKER, "".join(entry + "\n" for entry in warned)
163
+ )
164
+ except OSError:
165
+ pass # state dir unwritable — still warn this once
166
+ return True
167
+
168
+
169
+ def _skip_with_message(msg: str) -> None:
170
+ """Pass through unchanged while surfacing ``msg`` to the user.
171
+
172
+ ``systemMessage`` is the protocol field both hosts surface for humans:
173
+ Cosh-NG renders it as a hook notification and copilot-shell records it in
174
+ its hook log, while neither injects it into the model context. The
175
+ warning is also printed to stderr so hosts that forward hook stderr get
176
+ the same text.
177
+ """
178
+ warn(msg)
179
+ print(json.dumps({"systemMessage": msg}))
180
+ sys.exit(0)
181
+
182
+
183
+ def _extract_tools(input_data: dict) -> tuple[object, bool]:
184
+ """Extract the tool declarations from a BeforeModel payload.
185
+
186
+ Returns ``(tools, declared)`` where ``declared`` is True when the host
187
+ carried a tools field at any known position — even an empty one.
188
+
189
+ ``config.tools`` is the canonical position (both copilot-shell's Hook
190
+ Translator and Cosh-NG put it there); the top-level ``tools`` is the
191
+ older position, kept for hosts that still emit it. Presence of the
192
+ canonical key decides, not its truthiness: a host that declares no tools
193
+ sends an empty canonical array, and falling through to a stale legacy
194
+ field there would compress declarations this request never carried. This
195
+ mirrors the host-side precedence.
196
+ """
197
+ llm_request = input_data.get("llm_request")
198
+ if not isinstance(llm_request, dict):
199
+ return None, False
200
+ config = llm_request.get("config")
201
+ if isinstance(config, dict) and "tools" in config:
202
+ return config["tools"], True
203
+ if "tools" in llm_request:
204
+ return llm_request.get("tools"), True
205
+ return None, False
206
+
207
+
48
208
  # -- main --------------------------------------------------------------------
49
209
 
50
210
 
@@ -68,21 +228,42 @@ def main() -> None:
68
228
  except (json.JSONDecodeError, EOFError, ValueError):
69
229
  warn("failed to read BeforeModel payload. Passing through unchanged.")
70
230
  skip()
231
+ if not isinstance(input_data, dict):
232
+ # No session_id available outside a dict payload — the shared
233
+ # "<no-session>" key still bounds this to one warning.
234
+ if _should_warn_no_tools(""):
235
+ _skip_with_message(
236
+ "BeforeModel payload is not a JSON object. Passing through "
237
+ "unchanged (warned once per session)."
238
+ )
239
+ skip()
71
240
 
72
- # 3. Extract tools array. `config.tools` is the canonical position (both
73
- # copilot-shell's Hook Translator and Cosh-NG put it there); the top-level
74
- # `tools` is the older position, kept for hosts that still emit it.
75
- # Presence of the canonical key decides, not its truthiness: a host that
76
- # declares no tools sends an empty canonical array, and falling through to
77
- # a stale legacy field there would compress declarations this request never
78
- # carried. This mirrors the host-side precedence.
79
- llm_request = input_data.get("llm_request", {})
80
- config = llm_request.get("config")
81
- if isinstance(config, dict) and "tools" in config:
82
- tools = config["tools"]
83
- else:
84
- tools = llm_request.get("tools")
241
+ # 3. Extract tools array.
242
+ tools, tools_declared = _extract_tools(input_data)
85
243
  if not tools:
244
+ # A host that declares no tools for this turn (empty canonical array)
245
+ # skips silently — that is a normal request. But a BeforeModel event
246
+ # with no tools field at any known position means there is nothing
247
+ # for schema compression to work on. That skip used to be silent, so
248
+ # "0 schema records" could not be told apart from "hook never ran";
249
+ # warn once per session instead.
250
+ if not tools_declared and _should_warn_no_tools(
251
+ str(input_data.get("session_id", ""))
252
+ ):
253
+ if not isinstance(input_data.get("llm_request"), dict):
254
+ _skip_with_message(
255
+ "BeforeModel payload carries no llm_request object, so schema "
256
+ "compression cannot find tool declarations. Passing through "
257
+ "unchanged (warned once per session)."
258
+ )
259
+ else:
260
+ _skip_with_message(
261
+ "BeforeModel event carries no tool declarations at "
262
+ "llm_request.config.tools or llm_request.tools, so schema "
263
+ "compression was skipped. If the host declares tools, its "
264
+ "event format may not match the hook. Passing through "
265
+ "unchanged (warned once per session)."
266
+ )
86
267
  skip()
87
268
 
88
269
  tools_json = json.dumps(tools, separators=(",", ":"))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anolisa/dsh-tokenless",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Native DeepSeek Harness response compression for Tokenless",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,5 +1,5 @@
1
1
  name: tokenless
2
- version: "0.7.11"
2
+ version: "0.7.12"
3
3
  description: "Token-Less context compression for Hermes Agent — response compression, TOON encoding, command rewriting, and registered but hard-disabled Tool Ready"
4
4
  author: ANOLISA
5
5
  requires_env: []
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "component": "tokenless",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "targets": {
5
5
  "cosh": {
6
6
  "compatibleVersions": "*",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "tokenless",
3
3
  "name": "Tokenless",
4
- "version": "0.7.11",
4
+ "version": "0.7.12",
5
5
  "description": "Unified RTK command rewriting + response/TOON compression + registered but hard-disabled Tool Ready. Wraps tokenless and rtk system binaries via child_process — this is expected and not malicious.",
6
6
  "activation": {
7
7
  "onCapabilities": ["hook"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenless/openclaw-plugin",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Unified OpenClaw plugin — RTK command rewriting + tokenless schema/response compression for 60-90% LLM token savings",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Token-Less LLM Token Optimization Toolkit for Qoder CLI",
5
5
  "author": {
6
6
  "name": "Shile Zhang",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenless",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Token-Less context compression for Qwen Code — RTK command rewriting, response/TOON/schema compression, and registered but hard-disabled Tool Ready",
5
5
  "author": { "name": "ANOLISA" },
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "anolisa-tokenless",
3
3
  "type": "module",
4
- "version": "0.7.11",
4
+ "version": "0.7.12",
5
5
  "description": "Token-Less — LLM token optimization toolkit (schema/response compression, command rewriting, tool readiness)",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -44,10 +44,10 @@
44
44
  "arm64"
45
45
  ],
46
46
  "optionalDependencies": {
47
- "@anolisa/tokenless-linux-x64": "0.7.11",
48
- "@anolisa/tokenless-linux-arm64": "0.7.11",
49
- "@anolisa/tokenless-darwin-x64": "0.7.11",
50
- "@anolisa/tokenless-darwin-arm64": "0.7.11"
47
+ "@anolisa/tokenless-linux-x64": "0.7.12",
48
+ "@anolisa/tokenless-linux-arm64": "0.7.12",
49
+ "@anolisa/tokenless-darwin-x64": "0.7.12",
50
+ "@anolisa/tokenless-darwin-arm64": "0.7.12"
51
51
  },
52
52
  "publishConfig": {
53
53
  "registry": "https://registry.npmjs.org/",