@nextclaw/nextclaw-hermes-acp-bridge 0.1.1 → 0.1.3

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
@@ -5,3 +5,33 @@ This package contains Hermes-specific ACP bridge helpers.
5
5
  It is intentionally separate from the generic
6
6
  `@nextclaw/nextclaw-ncp-runtime-stdio-client` package so the stdio runtime stays
7
7
  protocol-generic while Hermes keeps its own integration layer.
8
+
9
+ ## Boundary
10
+
11
+ - This package exists to adapt Hermes into NextClaw. It is the primary place
12
+ for NextClaw-side bridge logic, launch-time injection, and compatibility
13
+ handling around Hermes ACP integration.
14
+ - Do not directly modify Hermes upstream source code when fixing NextClaw
15
+ integration issues. In this repo, Hermes should be treated as an upstream
16
+ dependency boundary.
17
+ - If Hermes behavior needs to be adjusted for NextClaw, prefer one of these
18
+ layers instead:
19
+ - bridge logic inside `packages/nextclaw-hermes-acp-bridge`
20
+ - NextClaw runtime adapter logic
21
+ - startup environment injection / wrapper behavior
22
+ - repo-local documentation and guardrails
23
+
24
+ ## Execution Model
25
+
26
+ - Hermes ACP execution in NextClaw is request-scoped, not session-agent-scoped.
27
+ - The long-lived ACP session keeps conversation history, cwd, selected model
28
+ snapshot, lightweight session metadata, and the current execution agent.
29
+ - Each prompt treats `nextclaw_narp.providerRoute` as execution truth. When the
30
+ prompt route changes, NextClaw rebuilds the Hermes execution agent for that
31
+ request and replaces the session's current execution agent with the new one.
32
+ - `providerRoute` is the execution truth for Hermes ACP requests. NextClaw does
33
+ not rely on ACP `setSessionModel(modelId)` to perform real cross-provider
34
+ switching for Hermes.
35
+ - Request-scoped execution agents must rebuild Hermes's cached system prompt
36
+ from the current route. They must not inherit a previous model/provider
37
+ prompt cache across cross-provider switches.
@@ -0,0 +1,186 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from typing import Any, Dict, Optional
7
+
8
+ LOGGER = logging.getLogger("nextclaw.hermes_acp_route_bridge")
9
+ API_MODE_HEADER = "x-nextclaw-narp-api-mode"
10
+ NARP_PROMPT_META_KEY = "nextclaw_narp"
11
+ _SESSION_ROUTE_OVERRIDES: Dict[str, Dict[str, Any]] = {}
12
+
13
+
14
+ def read_text_env(name: str) -> Optional[str]:
15
+ value = os.environ.get(name)
16
+ if not isinstance(value, str):
17
+ return None
18
+ trimmed = value.strip()
19
+ return trimmed or None
20
+
21
+
22
+ def _read_headers_env() -> Dict[str, str]:
23
+ raw = read_text_env("NEXTCLAW_HEADERS_JSON")
24
+ if not raw:
25
+ return {}
26
+ try:
27
+ parsed = json.loads(raw)
28
+ except Exception:
29
+ LOGGER.debug("Failed to parse NEXTCLAW_HEADERS_JSON", exc_info=True)
30
+ return {}
31
+ if not isinstance(parsed, dict):
32
+ return {}
33
+ normalized: Dict[str, str] = {}
34
+ for key, value in parsed.items():
35
+ if isinstance(key, str) and isinstance(value, str) and key.strip() and value.strip():
36
+ normalized[key] = value
37
+ return normalized
38
+
39
+
40
+ def _normalize_route_headers(raw_headers: Any) -> Dict[str, str]:
41
+ if not isinstance(raw_headers, dict):
42
+ return {}
43
+ normalized: Dict[str, str] = {}
44
+ for key, value in raw_headers.items():
45
+ if isinstance(key, str) and isinstance(value, str):
46
+ trimmed_key = key.strip()
47
+ trimmed_value = value.strip()
48
+ if trimmed_key and trimmed_value:
49
+ normalized[trimmed_key] = trimmed_value
50
+ return normalized
51
+
52
+
53
+ def _normalize_route_payload(raw_route: Any) -> Optional[Dict[str, Any]]:
54
+ if not isinstance(raw_route, dict):
55
+ return None
56
+ model = raw_route.get("model")
57
+ api_base = raw_route.get("apiBase")
58
+ if api_base is None:
59
+ api_base = raw_route.get("api_base")
60
+ api_key = raw_route.get("apiKey")
61
+ if api_key is None:
62
+ api_key = raw_route.get("api_key")
63
+ api_mode = raw_route.get("apiMode")
64
+ if api_mode is None:
65
+ api_mode = raw_route.get("api_mode")
66
+ provider = raw_route.get("provider")
67
+ headers = (
68
+ _normalize_route_headers(raw_route.get("headers"))
69
+ or _normalize_route_headers(raw_route.get("extraHeaders"))
70
+ or _normalize_route_headers(raw_route.get("extra_headers"))
71
+ )
72
+ api_mode_from_headers = headers.pop(API_MODE_HEADER, None)
73
+
74
+ normalized_model = model.strip() if isinstance(model, str) else None
75
+ normalized_api_base = api_base.strip() if isinstance(api_base, str) else None
76
+ normalized_api_key = api_key.strip() if isinstance(api_key, str) else None
77
+ normalized_api_mode = api_mode.strip() if isinstance(api_mode, str) else None
78
+ normalized_provider = provider.strip() if isinstance(provider, str) else None
79
+
80
+ if not any(
81
+ [
82
+ normalized_model,
83
+ normalized_api_base,
84
+ normalized_api_key,
85
+ headers,
86
+ normalized_api_mode,
87
+ normalized_provider,
88
+ ]
89
+ ):
90
+ return None
91
+
92
+ return {
93
+ "model": normalized_model,
94
+ "api_base": normalized_api_base,
95
+ "api_key": normalized_api_key or "",
96
+ "headers": headers,
97
+ "api_mode": normalized_api_mode or api_mode_from_headers or "chat_completions",
98
+ "provider": normalized_provider,
99
+ }
100
+
101
+
102
+ def _merge_route_payloads(
103
+ base_route: Optional[Dict[str, Any]],
104
+ override_route: Optional[Dict[str, Any]],
105
+ ) -> Optional[Dict[str, Any]]:
106
+ if base_route is None:
107
+ return dict(override_route) if override_route is not None else None
108
+ if override_route is None:
109
+ return dict(base_route)
110
+
111
+ return {
112
+ "model": override_route.get("model") or base_route.get("model"),
113
+ "api_base": override_route.get("api_base") or base_route.get("api_base"),
114
+ "api_key": override_route.get("api_key")
115
+ if override_route.get("api_key") is not None
116
+ else base_route.get("api_key"),
117
+ "headers": dict(override_route.get("headers") or {}),
118
+ "api_mode": override_route.get("api_mode") or base_route.get("api_mode"),
119
+ "provider": override_route.get("provider") or base_route.get("provider"),
120
+ }
121
+
122
+
123
+ def read_nextclaw_route(
124
+ session_id: Optional[str] = None,
125
+ explicit_model: Optional[str] = None,
126
+ explicit_base_url: Optional[str] = None,
127
+ explicit_api_mode: Optional[str] = None,
128
+ explicit_provider: Optional[str] = None,
129
+ ) -> Optional[Dict[str, Any]]:
130
+ model = explicit_model or read_text_env("NEXTCLAW_MODEL")
131
+ api_base = read_text_env("NEXTCLAW_API_BASE")
132
+ api_key = read_text_env("NEXTCLAW_API_KEY")
133
+ headers = _read_headers_env()
134
+ api_mode = headers.pop(API_MODE_HEADER, None) or "chat_completions"
135
+ route = _normalize_route_payload(
136
+ {
137
+ "model": model,
138
+ "api_base": api_base,
139
+ "api_key": api_key,
140
+ "headers": headers,
141
+ "api_mode": explicit_api_mode or api_mode,
142
+ "provider": explicit_provider,
143
+ }
144
+ )
145
+ if session_id:
146
+ route = _merge_route_payloads(route, _SESSION_ROUTE_OVERRIDES.get(session_id))
147
+ if route is None:
148
+ return None
149
+ if explicit_model:
150
+ route["model"] = explicit_model
151
+ if explicit_base_url:
152
+ route["api_base"] = explicit_base_url
153
+ if explicit_api_mode:
154
+ route["api_mode"] = explicit_api_mode
155
+ if explicit_provider:
156
+ route["provider"] = explicit_provider
157
+ return route
158
+
159
+
160
+ def read_nextclaw_prompt_route(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
161
+ nextclaw_meta = kwargs.get(NARP_PROMPT_META_KEY)
162
+ if not isinstance(nextclaw_meta, dict):
163
+ meta = kwargs.get("_meta")
164
+ if not isinstance(meta, dict):
165
+ return None
166
+ nextclaw_meta = meta.get(NARP_PROMPT_META_KEY)
167
+ if not isinstance(nextclaw_meta, dict):
168
+ return None
169
+ return _normalize_route_payload(nextclaw_meta.get("providerRoute"))
170
+
171
+
172
+ def remember_session_route(session_id: str, route: Dict[str, Any]) -> None:
173
+ _SESSION_ROUTE_OVERRIDES[session_id] = dict(route)
174
+
175
+
176
+ def read_session_route(session_id: str) -> Optional[Dict[str, Any]]:
177
+ route = _SESSION_ROUTE_OVERRIDES.get(session_id)
178
+ return dict(route) if route is not None else None
179
+
180
+
181
+ def pop_session_route(session_id: str) -> None:
182
+ _SESSION_ROUTE_OVERRIDES.pop(session_id, None)
183
+
184
+
185
+ def clear_session_routes() -> None:
186
+ _SESSION_ROUTE_OVERRIDES.clear()
@@ -7,57 +7,29 @@ NextClaw RuntimeRoute instead of resolving its own provider config first.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import json
10
+ import importlib.util
11
11
  import logging
12
- import os
13
- from typing import Any, Dict, Optional
12
+ from pathlib import Path
13
+ from typing import Any, Dict
14
14
 
15
15
  LOGGER = logging.getLogger("nextclaw.hermes_acp_route_bridge")
16
16
  ROUTE_ENABLE_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE"
17
- API_MODE_HEADER = "x-nextclaw-narp-api-mode"
18
-
19
-
20
- def _read_text_env(name: str) -> Optional[str]:
21
- value = os.environ.get(name)
22
- if not isinstance(value, str):
23
- return None
24
- trimmed = value.strip()
25
- return trimmed or None
26
-
27
-
28
- def _read_headers_env() -> Dict[str, str]:
29
- raw = _read_text_env("NEXTCLAW_HEADERS_JSON")
30
- if not raw:
31
- return {}
32
- try:
33
- parsed = json.loads(raw)
34
- except Exception:
35
- LOGGER.debug("Failed to parse NEXTCLAW_HEADERS_JSON", exc_info=True)
36
- return {}
37
- if not isinstance(parsed, dict):
38
- return {}
39
- normalized: Dict[str, str] = {}
40
- for key, value in parsed.items():
41
- if isinstance(key, str) and isinstance(value, str) and key.strip() and value.strip():
42
- normalized[key] = value
43
- return normalized
44
-
45
-
46
- def _read_nextclaw_route(explicit_model: Optional[str] = None) -> Optional[Dict[str, Any]]:
47
- model = explicit_model or _read_text_env("NEXTCLAW_MODEL")
48
- api_base = _read_text_env("NEXTCLAW_API_BASE")
49
- api_key = _read_text_env("NEXTCLAW_API_KEY")
50
- headers = _read_headers_env()
51
- api_mode = headers.pop(API_MODE_HEADER, None) or "chat_completions"
52
- if not any([model, api_base, api_key, headers]):
53
- return None
54
- return {
55
- "model": model,
56
- "api_base": api_base,
57
- "api_key": api_key or "",
58
- "headers": headers,
59
- "api_mode": api_mode,
60
- }
17
+ CREATE_EXECUTION_AGENT = None
18
+
19
+ _ROUTE_HELPER_SPEC = importlib.util.spec_from_file_location(
20
+ "nextclaw_hermes_acp_runtime_route",
21
+ Path(__file__).with_name("nextclaw-hermes-acp-runtime-route.py"),
22
+ )
23
+ if _ROUTE_HELPER_SPEC is None or _ROUTE_HELPER_SPEC.loader is None:
24
+ raise ImportError("Failed to load nextclaw Hermes ACP runtime route helper.")
25
+ _ROUTE_HELPER_MODULE = importlib.util.module_from_spec(_ROUTE_HELPER_SPEC)
26
+ _ROUTE_HELPER_SPEC.loader.exec_module(_ROUTE_HELPER_MODULE)
27
+ clear_session_routes = _ROUTE_HELPER_MODULE.clear_session_routes
28
+ pop_session_route = _ROUTE_HELPER_MODULE.pop_session_route
29
+ read_nextclaw_prompt_route = _ROUTE_HELPER_MODULE.read_nextclaw_prompt_route
30
+ read_nextclaw_route = _ROUTE_HELPER_MODULE.read_nextclaw_route
31
+ read_text_env = _ROUTE_HELPER_MODULE.read_text_env
32
+ remember_session_route = _ROUTE_HELPER_MODULE.remember_session_route
61
33
 
62
34
 
63
35
  def _merge_openai_headers(agent: Any, headers: Dict[str, str]) -> None:
@@ -103,7 +75,7 @@ def _patch_acp_auth() -> None:
103
75
  original_detect_provider = auth_module.detect_provider
104
76
 
105
77
  def detect_provider():
106
- route = _read_nextclaw_route()
78
+ route = read_nextclaw_route()
107
79
  if route is not None:
108
80
  return "nextclaw"
109
81
  return original_detect_provider()
@@ -116,6 +88,7 @@ def _patch_acp_auth() -> None:
116
88
 
117
89
 
118
90
  def _patch_session_manager() -> None:
91
+ global CREATE_EXECUTION_AGENT
119
92
  try:
120
93
  from acp_adapter import session as session_module
121
94
  except Exception:
@@ -124,6 +97,56 @@ def _patch_session_manager() -> None:
124
97
 
125
98
  original_make_agent = session_module.SessionManager._make_agent
126
99
 
100
+ def create_execution_agent(
101
+ *,
102
+ session_id: str,
103
+ cwd: str,
104
+ model: str | None,
105
+ requested_provider: str | None,
106
+ base_url: str | None,
107
+ api_mode: str | None,
108
+ ):
109
+ route = read_nextclaw_route(
110
+ session_id=session_id,
111
+ explicit_model=model,
112
+ explicit_base_url=base_url,
113
+ explicit_api_mode=api_mode,
114
+ explicit_provider=requested_provider,
115
+ )
116
+ if route is None:
117
+ raise RuntimeError(
118
+ "Missing NextClaw providerRoute for Hermes ACP request-scoped execution."
119
+ )
120
+
121
+ from run_agent import AIAgent
122
+
123
+ kwargs = {
124
+ "platform": "acp",
125
+ "enabled_toolsets": ["hermes-acp"],
126
+ "quiet_mode": True,
127
+ "session_id": session_id,
128
+ "model": route["model"] or "",
129
+ "provider": route.get("provider") or requested_provider or "custom",
130
+ "api_mode": route["api_mode"],
131
+ "base_url": route["api_base"] or "",
132
+ "api_key": route["api_key"],
133
+ }
134
+
135
+ session_module._register_task_cwd(session_id, cwd)
136
+ agent = AIAgent(**kwargs)
137
+ agent._print_fn = session_module._acp_stderr_print
138
+ _merge_openai_headers(agent, route["headers"])
139
+ _merge_anthropic_headers(agent, route["headers"])
140
+ LOGGER.info(
141
+ "Hermes ACP execution agent route resolved: session_id=%s model=%s provider=%s api_mode=%s base_url=%s",
142
+ session_id,
143
+ getattr(agent, "model", ""),
144
+ getattr(agent, "provider", ""),
145
+ getattr(agent, "api_mode", ""),
146
+ getattr(agent, "base_url", ""),
147
+ )
148
+ return agent
149
+
127
150
  def bridged_make_agent(
128
151
  self,
129
152
  *,
@@ -134,7 +157,13 @@ def _patch_session_manager() -> None:
134
157
  base_url: str | None = None,
135
158
  api_mode: str | None = None,
136
159
  ):
137
- route = _read_nextclaw_route(explicit_model=model)
160
+ route = read_nextclaw_route(
161
+ session_id=session_id,
162
+ explicit_model=model,
163
+ explicit_base_url=base_url,
164
+ explicit_api_mode=api_mode,
165
+ explicit_provider=requested_provider,
166
+ )
138
167
  if route is None:
139
168
  return original_make_agent(
140
169
  self,
@@ -149,28 +178,89 @@ def _patch_session_manager() -> None:
149
178
  if self._agent_factory is not None:
150
179
  return self._agent_factory()
151
180
 
152
- from run_agent import AIAgent
181
+ return create_execution_agent(
182
+ session_id=session_id,
183
+ cwd=cwd,
184
+ model=(route or {}).get("model") or model,
185
+ requested_provider=(route or {}).get("provider") or requested_provider,
186
+ base_url=(route or {}).get("api_base") or base_url,
187
+ api_mode=(route or {}).get("api_mode") or api_mode,
188
+ )
153
189
 
154
- kwargs = {
155
- "platform": "acp",
156
- "enabled_toolsets": ["hermes-acp"],
157
- "quiet_mode": True,
158
- "session_id": session_id,
159
- "model": route["model"] or "",
160
- "provider": requested_provider or "custom",
161
- "api_mode": api_mode or route["api_mode"],
162
- "base_url": base_url or route["api_base"] or "",
163
- "api_key": route["api_key"],
164
- }
190
+ session_module.SessionManager._make_agent = bridged_make_agent
165
191
 
166
- session_module._register_task_cwd(session_id, cwd)
167
- agent = AIAgent(**kwargs)
168
- agent._print_fn = session_module._acp_stderr_print
169
- _merge_openai_headers(agent, route["headers"])
170
- _merge_anthropic_headers(agent, route["headers"])
171
- return agent
192
+ original_remove_session = session_module.SessionManager.remove_session
172
193
 
173
- session_module.SessionManager._make_agent = bridged_make_agent
194
+ def bridged_remove_session(self, session_id: str) -> bool:
195
+ pop_session_route(session_id)
196
+ return original_remove_session(self, session_id)
197
+
198
+ session_module.SessionManager.remove_session = bridged_remove_session
199
+
200
+ original_cleanup = session_module.SessionManager.cleanup
201
+
202
+ def bridged_cleanup(self) -> None:
203
+ clear_session_routes()
204
+ return original_cleanup(self)
205
+
206
+ session_module.SessionManager.cleanup = bridged_cleanup
207
+ CREATE_EXECUTION_AGENT = create_execution_agent
208
+
209
+
210
+ def _patch_prompt_execution() -> None:
211
+ try:
212
+ from acp_adapter import server as server_module
213
+ from acp_adapter import session as session_module
214
+ except Exception:
215
+ LOGGER.debug("Failed to import Hermes ACP server/session modules", exc_info=True)
216
+ return
217
+
218
+ original_prompt = getattr(server_module.HermesACPAgent, "prompt", None)
219
+ if not callable(original_prompt):
220
+ return
221
+ if getattr(original_prompt, "__nextclaw_request_scoped_agent_bridge__", False):
222
+ return
223
+
224
+ async def bridged_prompt(self, prompt, session_id: str, **kwargs):
225
+ if not callable(CREATE_EXECUTION_AGENT):
226
+ raise RuntimeError("NextClaw Hermes ACP request-scoped agent bridge is not ready.")
227
+ state = self.session_manager.get_session(session_id)
228
+ if state is None:
229
+ return await original_prompt(self, prompt, session_id, **kwargs)
230
+
231
+ prompt_route = read_nextclaw_prompt_route(kwargs)
232
+ if prompt_route is not None:
233
+ remember_session_route(session_id, prompt_route)
234
+ if prompt_route.get("model"):
235
+ state.model = prompt_route["model"]
236
+
237
+ previous_agent = getattr(state, "agent", None)
238
+ provider_hint = None
239
+ base_url_hint = None
240
+ api_mode_hint = None
241
+ try:
242
+ state.agent = CREATE_EXECUTION_AGENT(
243
+ session_id=session_id,
244
+ cwd=state.cwd,
245
+ model=state.model or getattr(previous_agent, "model", None),
246
+ requested_provider=provider_hint,
247
+ base_url=base_url_hint,
248
+ api_mode=api_mode_hint,
249
+ )
250
+ session_module._register_task_cwd(session_id, state.cwd)
251
+ except Exception as exc:
252
+ LOGGER.exception(
253
+ "Failed to create prompt-routed Hermes execution agent for session %s",
254
+ session_id,
255
+ )
256
+ raise RuntimeError(
257
+ "NextClaw Hermes ACP failed to create the prompt-routed execution agent."
258
+ ) from exc
259
+
260
+ return await original_prompt(self, prompt, session_id, **kwargs)
261
+
262
+ bridged_prompt.__nextclaw_request_scoped_agent_bridge__ = True
263
+ server_module.HermesACPAgent.prompt = bridged_prompt
174
264
 
175
265
 
176
266
  def _patch_acp_reasoning_mapping() -> None:
@@ -215,10 +305,11 @@ def _patch_acp_reasoning_mapping() -> None:
215
305
 
216
306
 
217
307
  def _activate() -> None:
218
- if _read_text_env(ROUTE_ENABLE_ENV) != "1":
308
+ if read_text_env(ROUTE_ENABLE_ENV) != "1":
219
309
  return
220
310
  _patch_acp_auth()
221
311
  _patch_session_manager()
312
+ _patch_prompt_execution()
222
313
  _patch_acp_reasoning_mapping()
223
314
 
224
315
 
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { basename, delimiter } from "node:path";
2
+ import { basename, delimiter, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  //#region src/hermes-acp-route-bridge.utils.ts
5
5
  const HERMES_ACP_BRIDGE_ENABLE_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE";
@@ -34,8 +34,11 @@ function isHermesAcpRuntimeConfig(config) {
34
34
  return false;
35
35
  }
36
36
  function resolveHermesAcpBridgeDir() {
37
- const bridgeDir = fileURLToPath(new URL("./hermes-acp-route-bridge", import.meta.url));
38
- return existsSync(bridgeDir) ? bridgeDir : null;
37
+ const packageDir = fileURLToPath(new URL("../", import.meta.url));
38
+ const sourceBridgeDir = resolve(packageDir, "src/hermes-acp-route-bridge");
39
+ if (existsSync(sourceBridgeDir)) return sourceBridgeDir;
40
+ const distBridgeDir = resolve(packageDir, "dist/hermes-acp-route-bridge");
41
+ return existsSync(distBridgeDir) ? distBridgeDir : null;
39
42
  }
40
43
  function mergePathList(existingValue, injectedPath) {
41
44
  const parts = (existingValue ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/nextclaw-hermes-acp-bridge",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Hermes-specific ACP bridge helpers used by NextClaw runtime integrations.",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "README.md"
17
17
  ],
18
18
  "dependencies": {
19
- "@nextclaw/ncp": "0.5.2"
19
+ "@nextclaw/ncp": "0.5.4"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.17.6",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "scripts": {
27
27
  "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle && node ./scripts/copy-hermes-acp-route-bridge.mjs",
28
+ "check:bridge-sync": "node ./scripts/check-hermes-acp-route-bridge-sync.mjs",
28
29
  "lint": "eslint .",
29
30
  "tsc": "tsc -p tsconfig.json",
30
31
  "test": "vitest run"