@nextclaw/nextclaw-ncp-runtime-stdio-client 0.1.1 → 0.1.2

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv } from "./stdio-runtime-config.utils.js";
1
+ import { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv } from "./stdio-runtime-config.utils.js";
2
2
  import { StdioRuntimeNcpAgentRuntime, StdioRuntimeNcpAgentRuntimeConfig } from "./stdio-runtime.service.js";
3
3
  import { probeStdioRuntime } from "./stdio-runtime-probe.utils.js";
4
- export { NARP_STDIO_PROMPT_META_KEY, type NarpStdioPromptMeta, StdioRuntimeConfigResolver, type StdioRuntimeEnv, StdioRuntimeNcpAgentRuntime, type StdioRuntimeNcpAgentRuntimeConfig, type StdioRuntimeProcessScope, type StdioRuntimeResolvedConfig, type StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, probeStdioRuntime };
4
+ export { NARP_STDIO_PROMPT_META_KEY, type NarpStdioPromptMeta, StdioRuntimeConfigResolver, type StdioRuntimeEnv, StdioRuntimeNcpAgentRuntime, type StdioRuntimeNcpAgentRuntimeConfig, type StdioRuntimeProcessScope, type StdioRuntimeResolvedConfig, type StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv, probeStdioRuntime };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv } from "./stdio-runtime-config.utils.js";
1
+ import { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv } from "./stdio-runtime-config.utils.js";
2
2
  import { StdioRuntimeNcpAgentRuntime } from "./stdio-runtime.service.js";
3
3
  import { probeStdioRuntime } from "./stdio-runtime-probe.utils.js";
4
- export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, StdioRuntimeNcpAgentRuntime, buildRuntimeRouteBridgeEnv, probeStdioRuntime };
4
+ export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, StdioRuntimeNcpAgentRuntime, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv, probeStdioRuntime };
@@ -32,5 +32,10 @@ declare class StdioRuntimeConfigResolver {
32
32
  declare function buildRuntimeRouteBridgeEnv(params: {
33
33
  providerRoute?: NcpProviderRuntimeRoute;
34
34
  }): Record<string, string>;
35
+ declare function buildStdioRuntimeLaunchEnv(params: {
36
+ baseEnv?: Record<string, string | undefined>;
37
+ configEnv?: StdioRuntimeEnv;
38
+ providerRoute?: NcpProviderRuntimeRoute;
39
+ }): Record<string, string>;
35
40
  //#endregion
36
- export { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv };
41
+ export { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv };
@@ -82,6 +82,15 @@ function buildRuntimeRouteBridgeEnv(params) {
82
82
  [DEFAULT_RUNTIME_ROUTE_BRIDGE_FIELDS.headers]: JSON.stringify(providerRoute.headers ?? {})
83
83
  };
84
84
  }
85
+ function buildStdioRuntimeLaunchEnv(params) {
86
+ const baseEnv = {};
87
+ for (const [key, value] of Object.entries(params.baseEnv ?? process.env)) if (typeof value === "string") baseEnv[key] = value;
88
+ return {
89
+ ...baseEnv,
90
+ ...params.configEnv ?? {},
91
+ ...buildRuntimeRouteBridgeEnv({ providerRoute: params.providerRoute })
92
+ };
93
+ }
85
94
  function parseJsonArray(value) {
86
95
  if (typeof value !== "string" || !value.trim()) return;
87
96
  try {
@@ -99,4 +108,4 @@ function parseJsonObject(value) {
99
108
  }
100
109
  }
101
110
  //#endregion
102
- export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv, readString };
111
+ export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv, readString };
@@ -1,4 +1,4 @@
1
- import { buildStdioLaunchEnv } from "./hermes-acp-route-bridge.utils.js";
1
+ import { buildStdioRuntimeLaunchEnv } from "./stdio-runtime-config.utils.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { Readable, Writable } from "node:stream";
4
4
  import * as acp from "@agentclientprotocol/sdk";
@@ -12,10 +12,7 @@ const createProbeClientBridge = () => ({
12
12
  async function probeStdioRuntime(config) {
13
13
  const child = spawn(config.command, config.args, {
14
14
  cwd: config.cwd,
15
- env: buildStdioLaunchEnv({
16
- config,
17
- useProbeRoute: true
18
- }),
15
+ env: buildStdioRuntimeLaunchEnv({ configEnv: config.env }),
19
16
  stdio: [
20
17
  "pipe",
21
18
  "pipe",
@@ -1,5 +1,4 @@
1
- import { NARP_STDIO_PROMPT_META_KEY, readString } from "./stdio-runtime-config.utils.js";
2
- import { buildStdioLaunchEnv } from "./hermes-acp-route-bridge.utils.js";
1
+ import { NARP_STDIO_PROMPT_META_KEY, buildStdioRuntimeLaunchEnv, readString } from "./stdio-runtime-config.utils.js";
3
2
  import { randomUUID } from "node:crypto";
4
3
  import { spawn } from "node:child_process";
5
4
  import { Readable, Writable } from "node:stream";
@@ -156,8 +155,8 @@ var StdioRuntimeSession = class {
156
155
  ensureStarted = async (params) => {
157
156
  this.pendingProviderRoute = params?.providerRoute;
158
157
  if (this.connection && this.remoteSessionId) return;
159
- const env = buildStdioLaunchEnv({
160
- config: this.config,
158
+ const env = buildStdioRuntimeLaunchEnv({
159
+ configEnv: this.config.env,
161
160
  providerRoute: this.pendingProviderRoute
162
161
  });
163
162
  this.child = spawn(this.config.command, this.config.args, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/nextclaw-ncp-runtime-stdio-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Optional NCP runtime adapter backed by a protocol-aware stdio agent command.",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "vitest": "^4.1.2"
26
26
  },
27
27
  "scripts": {
28
- "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle && node ./scripts/copy-hermes-acp-route-bridge.mjs",
28
+ "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
29
29
  "lint": "eslint .",
30
30
  "tsc": "tsc -p tsconfig.json",
31
31
  "test": "vitest run"
@@ -1,225 +0,0 @@
1
- """NextClaw bridge for Hermes ACP RuntimeRoute passthrough.
2
-
3
- This module is auto-imported by Python when PYTHONPATH points at this
4
- directory. It patches Hermes ACP so `hermes acp` consumes the active
5
- NextClaw RuntimeRoute instead of resolving its own provider config first.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import logging
12
- import os
13
- from typing import Any, Dict, Optional
14
-
15
- LOGGER = logging.getLogger("nextclaw.hermes_acp_route_bridge")
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
- }
61
-
62
-
63
- def _merge_openai_headers(agent: Any, headers: Dict[str, str]) -> None:
64
- if not headers:
65
- return
66
- client_kwargs = getattr(agent, "_client_kwargs", None)
67
- if not isinstance(client_kwargs, dict):
68
- return
69
- merged = dict(client_kwargs.get("default_headers") or {})
70
- merged.update(headers)
71
- client_kwargs["default_headers"] = merged
72
- replace_client = getattr(agent, "_replace_primary_openai_client", None)
73
- if callable(replace_client):
74
- replace_client(reason="nextclaw_runtime_route_bridge")
75
-
76
-
77
- def _merge_anthropic_headers(agent: Any, headers: Dict[str, str]) -> None:
78
- if not headers:
79
- return
80
- client = getattr(agent, "_anthropic_client", None)
81
- if client is None:
82
- return
83
- merged = dict(getattr(client, "_default_headers", {}) or {})
84
- merged.update(headers)
85
- if hasattr(client, "_default_headers"):
86
- client._default_headers = merged
87
- inner_client = getattr(client, "_client", None)
88
- if inner_client is not None:
89
- if hasattr(inner_client, "_default_headers"):
90
- inner_client._default_headers = merged
91
- transport_headers = getattr(inner_client, "headers", None)
92
- if hasattr(transport_headers, "update"):
93
- transport_headers.update(merged)
94
-
95
-
96
- def _patch_acp_auth() -> None:
97
- try:
98
- from acp_adapter import auth as auth_module
99
- except Exception:
100
- LOGGER.debug("Failed to import acp_adapter.auth", exc_info=True)
101
- return
102
-
103
- original_detect_provider = auth_module.detect_provider
104
-
105
- def detect_provider():
106
- route = _read_nextclaw_route()
107
- if route is not None:
108
- return "nextclaw"
109
- return original_detect_provider()
110
-
111
- def has_provider():
112
- return detect_provider() is not None
113
-
114
- auth_module.detect_provider = detect_provider
115
- auth_module.has_provider = has_provider
116
-
117
-
118
- def _patch_session_manager() -> None:
119
- try:
120
- from acp_adapter import session as session_module
121
- except Exception:
122
- LOGGER.debug("Failed to import acp_adapter.session", exc_info=True)
123
- return
124
-
125
- original_make_agent = session_module.SessionManager._make_agent
126
-
127
- def bridged_make_agent(
128
- self,
129
- *,
130
- session_id: str,
131
- cwd: str,
132
- model: str | None = None,
133
- requested_provider: str | None = None,
134
- base_url: str | None = None,
135
- api_mode: str | None = None,
136
- ):
137
- route = _read_nextclaw_route(explicit_model=model)
138
- if route is None:
139
- return original_make_agent(
140
- self,
141
- session_id=session_id,
142
- cwd=cwd,
143
- model=model,
144
- requested_provider=requested_provider,
145
- base_url=base_url,
146
- api_mode=api_mode,
147
- )
148
-
149
- if self._agent_factory is not None:
150
- return self._agent_factory()
151
-
152
- from run_agent import AIAgent
153
-
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
- }
165
-
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
172
-
173
- session_module.SessionManager._make_agent = bridged_make_agent
174
-
175
-
176
- def _patch_acp_reasoning_mapping() -> None:
177
- try:
178
- from run_agent import AIAgent
179
- except Exception:
180
- LOGGER.debug("Failed to import run_agent.AIAgent", exc_info=True)
181
- return
182
-
183
- original_run_conversation = getattr(AIAgent, "run_conversation", None)
184
- if not callable(original_run_conversation):
185
- return
186
- if getattr(original_run_conversation, "__nextclaw_reasoning_bridge__", False):
187
- return
188
-
189
- def bridged_run_conversation(self, *args, **kwargs):
190
- original_thinking = getattr(self, "thinking_callback", None)
191
- original_reasoning = getattr(self, "reasoning_callback", None)
192
- remapped = (
193
- getattr(self, "platform", None) == "acp"
194
- and callable(original_thinking)
195
- and original_reasoning is None
196
- )
197
-
198
- if remapped:
199
- # Hermes ACP currently wires its transient spinner/status callback
200
- # into ACP thought events. For ACP sessions, remap that callback to
201
- # the real reasoning channel so downstream clients receive model
202
- # reasoning instead of strings like "(⌐■_■) computing...".
203
- self.reasoning_callback = original_thinking
204
- self.thinking_callback = None
205
-
206
- try:
207
- return original_run_conversation(self, *args, **kwargs)
208
- finally:
209
- if remapped:
210
- self.thinking_callback = original_thinking
211
- self.reasoning_callback = original_reasoning
212
-
213
- bridged_run_conversation.__nextclaw_reasoning_bridge__ = True
214
- AIAgent.run_conversation = bridged_run_conversation
215
-
216
-
217
- def _activate() -> None:
218
- if _read_text_env(ROUTE_ENABLE_ENV) != "1":
219
- return
220
- _patch_acp_auth()
221
- _patch_session_manager()
222
- _patch_acp_reasoning_mapping()
223
-
224
-
225
- _activate()
@@ -1,61 +0,0 @@
1
- import { buildRuntimeRouteBridgeEnv } from "./stdio-runtime-config.utils.js";
2
- import { existsSync } from "node:fs";
3
- import { basename, delimiter } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- //#region src/hermes-acp-route-bridge.utils.ts
6
- const HERMES_ACP_BRIDGE_ENABLE_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE";
7
- const HERMES_ACP_BRIDGE_DIR_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE_DIR";
8
- const HERMES_ACP_PROBE_ROUTE = {
9
- model: "nextclaw-hermes-acp-probe",
10
- apiBase: "http://127.0.0.1:1/v1",
11
- apiKey: "nextclaw-hermes-acp-probe-key",
12
- headers: { "x-nextclaw-narp-api-mode": "chat_completions" }
13
- };
14
- function normalizeCommandBasename(command) {
15
- return basename(command).trim().toLowerCase();
16
- }
17
- function isPythonCommand(command) {
18
- const normalized = normalizeCommandBasename(command);
19
- return normalized === "python" || normalized === "python3" || normalized.startsWith("python3.");
20
- }
21
- function targetsHermesAcpModule(args) {
22
- for (let index = 0; index < args.length - 1; index += 1) if (args[index] === "-m" && args[index + 1]?.trim().toLowerCase() === "acp_adapter.entry") return true;
23
- return false;
24
- }
25
- function isHermesAcpRuntimeConfig(config) {
26
- const command = normalizeCommandBasename(config.command);
27
- if (command === "hermes-acp") return true;
28
- if (command === "hermes") return config.args[0]?.trim().toLowerCase() === "acp";
29
- if (isPythonCommand(config.command)) return targetsHermesAcpModule(config.args);
30
- return false;
31
- }
32
- function resolveHermesAcpBridgeDir() {
33
- const bridgeDir = fileURLToPath(new URL("./hermes-acp-route-bridge", import.meta.url));
34
- return existsSync(bridgeDir) ? bridgeDir : null;
35
- }
36
- function mergePathList(existingValue, injectedPath) {
37
- const parts = (existingValue ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
38
- if (!parts.includes(injectedPath)) parts.unshift(injectedPath);
39
- return parts.join(delimiter);
40
- }
41
- function buildStdioLaunchEnv(params) {
42
- const providerRoute = params.providerRoute ?? (params.useProbeRoute && isHermesAcpRuntimeConfig(params.config) ? HERMES_ACP_PROBE_ROUTE : void 0);
43
- const baseEnv = {};
44
- for (const [key, value] of Object.entries(params.baseEnv ?? process.env)) if (typeof value === "string") baseEnv[key] = value;
45
- const env = {
46
- ...baseEnv,
47
- ...params.config.env ?? {},
48
- ...buildRuntimeRouteBridgeEnv({ providerRoute })
49
- };
50
- if (!isHermesAcpRuntimeConfig(params.config)) return env;
51
- const bridgeDir = resolveHermesAcpBridgeDir();
52
- if (!bridgeDir) return env;
53
- return {
54
- ...env,
55
- [HERMES_ACP_BRIDGE_ENABLE_ENV]: "1",
56
- [HERMES_ACP_BRIDGE_DIR_ENV]: bridgeDir,
57
- PYTHONPATH: mergePathList(env.PYTHONPATH, bridgeDir)
58
- };
59
- }
60
- //#endregion
61
- export { buildStdioLaunchEnv };