@zeulewan/glueclaw-provider 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@ export GLUECLAW_REQUEST_TIMEOUT_MS=600000
66
66
 
67
67
  - Tested with Telegram and OpenClaw TUI
68
68
  - Switching between GlueClaw and other backends (e.g. Codex) works seamlessly via `/model`
69
- - The installer patches one file in OpenClaw's dist to expose the MCP loopback token to plugins. A `.glueclaw-bak` backup is created.
69
+ - The installer does not patch OpenClaw's dist. GlueClaw starts the MCP loopback in-process when available.
70
70
 
71
71
  ## Disclaimer
72
72
 
package/install.sh CHANGED
@@ -29,14 +29,6 @@ oc_config() {
29
29
  }
30
30
  }
31
31
 
32
- sedi() {
33
- if [[ "$(uname)" == "Darwin" ]]; then
34
- sed -i '' "$@"
35
- else
36
- sed -i "$@"
37
- fi
38
- }
39
-
40
32
  require_cmd() {
41
33
  command -v "$1" >/dev/null 2>&1 || die "$1 not found. $2"
42
34
  }
@@ -121,14 +113,6 @@ else
121
113
  fi
122
114
  echo ""
123
115
 
124
- # Find OpenClaw dist
125
- OPENCLAW_BIN="$(command -v openclaw)"
126
- OPENCLAW_ROOT="$(dirname "$OPENCLAW_BIN")/../lib/node_modules/openclaw"
127
- # Suppress not-found: fallback path may not exist
128
- [ ! -d "$OPENCLAW_ROOT/dist" ] && OPENCLAW_ROOT="$(npm root -g 2>/dev/null)/openclaw"
129
- [ ! -d "$OPENCLAW_ROOT/dist" ] && die "Cannot find OpenClaw dist"
130
- OPENCLAW_DIST="$OPENCLAW_ROOT/dist"
131
-
132
116
  # Detect shell config
133
117
  if [ -f "${HOME}/.zshrc" ]; then
134
118
  SHELL_RC="${HOME}/.zshrc"
@@ -142,17 +126,7 @@ fi
142
126
 
143
127
  GW_PID=""
144
128
  GW_LOG=""
145
- BACKUP_FILE=""
146
129
  cleanup() {
147
- # Restore MCP patch backup if script failed mid-patch
148
- if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then
149
- if [ -w "$(dirname "$BACKUP_FILE")" ]; then
150
- mv "$BACKUP_FILE" "${BACKUP_FILE%.glueclaw-bak}" 2>/dev/null || true
151
- else
152
- sudo mv "$BACKUP_FILE" "${BACKUP_FILE%.glueclaw-bak}" 2>/dev/null || true
153
- fi
154
- echo " Restored backup: $(basename "$BACKUP_FILE")" >&2
155
- fi
156
130
  if [ -n "$GW_PID" ] && kill -0 "$GW_PID" 2>/dev/null; then
157
131
  kill "$GW_PID" 2>/dev/null || true
158
132
  fi
@@ -162,19 +136,19 @@ trap cleanup INT TERM
162
136
 
163
137
  # --- 1. Dependencies ---
164
138
 
165
- echo "[1/7] Installing dependencies..."
139
+ echo "[1/6] Installing dependencies..."
166
140
  cd "$PLUGIN_DIR"
167
141
  npm install --silent || die "npm install failed"
168
142
 
169
143
  # --- 2. Environment ---
170
144
 
171
- echo "[2/7] Setting up environment..."
145
+ echo "[2/6] Setting up environment..."
172
146
  ensure_line "$SHELL_RC" "GLUECLAW_KEY" "export GLUECLAW_KEY=local"
173
147
  export GLUECLAW_KEY=local
174
148
 
175
149
  # --- 3. Plugin registration ---
176
150
 
177
- echo "[3/7] Registering plugin..."
151
+ echo "[3/6] Registering plugin..."
178
152
  # GlueClaw is on OpenClaw's official safe plugin list. Try standard install first,
179
153
  # fall back to --dangerously-force-unsafe-install for older OpenClaw versions,
180
154
  # then manual config as last resort.
@@ -188,7 +162,7 @@ fi
188
162
 
189
163
  # --- 4. Model config ---
190
164
 
191
- echo "[4/7] Configuring models..."
165
+ echo "[4/6] Configuring models..."
192
166
  # These two are fatal — without them, nothing works
193
167
  oc_config models.providers.glueclaw \
194
168
  '{"baseUrl":"local://glueclaw","models":[{"id":"glueclaw-opus","name":"GlueClaw Opus","contextWindow":1000000},{"id":"glueclaw-sonnet","name":"GlueClaw Sonnet","contextWindow":1000000},{"id":"glueclaw-haiku","name":"GlueClaw Haiku","contextWindow":200000}]}' \
@@ -211,47 +185,20 @@ oc_config gateway.tools.allow \
211
185
 
212
186
  # --- 5. Auth profile ---
213
187
 
214
- echo "[5/7] Setting up auth..."
188
+ echo "[5/6] Setting up auth..."
215
189
  AGENT_DIR="${HOME}/.openclaw/agents/main/agent"
216
190
  mkdir -p "$AGENT_DIR" || die "Cannot create $AGENT_DIR"
217
191
  AUTH_FILE="$AGENT_DIR/auth-profiles.json"
218
192
  write_auth_profile "$AUTH_FILE"
219
193
 
220
- # --- 6. Patch: MCP bridge ---
221
-
222
- echo "[6/7] Patching gateway for MCP bridge..."
223
- SERVER_FILE=$(grep -rl "mcp loopback listening" "$OPENCLAW_DIST"/*.js 2>/dev/null | head -n 1)
224
- [ -z "$SERVER_FILE" ] && die "Cannot find MCP loopback in OpenClaw dist — incompatible version?"
225
-
226
- # Use sudo for file operations if dist directory is not writable (global npm install)
227
- ELEVATE=""
228
- if [ ! -w "$OPENCLAW_DIST" ]; then
229
- echo " OpenClaw dist is root-owned, using sudo for patch..."
230
- ELEVATE="sudo"
231
- fi
232
-
233
- if ! grep -q "__GLUECLAW_MCP" "$SERVER_FILE"; then
234
- $ELEVATE cp "$SERVER_FILE" "${SERVER_FILE}.glueclaw-bak" || die "Cannot backup $SERVER_FILE"
235
- BACKUP_FILE="${SERVER_FILE}.glueclaw-bak"
236
- # shellcheck disable=SC2016
237
- if [ -n "$ELEVATE" ]; then
238
- $ELEVATE sed -i 's/logDebug(`mcp loopback listening/process.env.__GLUECLAW_MCP_PORT = String(address.port); process.env.__GLUECLAW_MCP_TOKEN = token; logDebug(`mcp loopback listening/' "$SERVER_FILE" ||
239
- die "Failed to patch $SERVER_FILE"
240
- else
241
- sedi 's/logDebug(`mcp loopback listening/process.env.__GLUECLAW_MCP_PORT = String(address.port); process.env.__GLUECLAW_MCP_TOKEN = token; logDebug(`mcp loopback listening/' "$SERVER_FILE" ||
242
- die "Failed to patch $SERVER_FILE"
243
- fi
244
- # Validate the patch actually applied
245
- grep -q "__GLUECLAW_MCP_PORT" "$SERVER_FILE" || die "MCP patch did not apply — sed replacement failed"
246
- BACKUP_FILE="" # Patch succeeded, don't restore on cleanup
247
- echo " Patched $(basename "$SERVER_FILE")"
248
- else
249
- echo " Already patched"
250
- fi
251
-
252
- # --- 7. Restart gateway ---
194
+ # --- 6. Restart gateway ---
195
+ # Note: GlueClaw bootstraps OpenClaw's MCP loopback in-process from
196
+ # src/stream.ts (see docs/RFC-001-sessions-send-native.md). No dist patching
197
+ # is needed GlueClaw shares OpenClaw's module cache as an in-process
198
+ # provider, so calling ensureMcpLoopbackServer() and getActiveMcpLoopbackRuntime()
199
+ # directly is sufficient.
253
200
 
254
- echo "[7/7] Starting gateway..."
201
+ echo "[6/6] Starting gateway..."
255
202
  # Stop any existing gateway first
256
203
  pkill -f "openclaw.*gateway" 2>/dev/null || true
257
204
  openclaw gateway stop 2>/dev/null || true
@@ -312,4 +259,4 @@ echo " Default: glueclaw/glueclaw-sonnet"
312
259
  echo ""
313
260
  echo " Run: openclaw tui"
314
261
  echo ""
315
- echo " Re-run after OpenClaw updates to re-apply patches."
262
+ echo " Re-run after OpenClaw updates to refresh plugin registration."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeulewan/glueclaw-provider",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "GlueClaw - Claude CLI subprocess provider for Max subscription",
5
5
  "type": "module",
6
6
  "engines": {
package/src/stream.ts CHANGED
@@ -7,12 +7,14 @@ import {
7
7
  rmSync,
8
8
  renameSync,
9
9
  } from "node:fs";
10
- import { join } from "node:path";
10
+ import { basename, delimiter, dirname, join, normalize } from "node:path";
11
11
  import { tmpdir } from "node:os";
12
12
  import { randomBytes } from "node:crypto";
13
+ import { pathToFileURL } from "node:url";
13
14
  import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
14
15
  import type { StreamFn } from "@mariozechner/pi-agent-core";
15
16
  import type { AssistantMessage, Usage, TextContent } from "@mariozechner/pi-ai";
17
+ import { deriveTurnSessionKey } from "./session-key.js";
16
18
 
17
19
  const PROCESS_TIMEOUT_MS = 5000;
18
20
  const REQUEST_TIMEOUT_MS = 120_000;
@@ -91,12 +93,99 @@ export function buildMsg(
91
93
  };
92
94
  }
93
95
 
94
- /** Get the MCP loopback port and token from process.env.
95
- * The gateway patches write these during MCP server startup. */
96
- export function getMcpLoopback(): { port: number; token: string } | undefined {
97
- const port = process.env.__GLUECLAW_MCP_PORT;
96
+ interface McpLoopbackRuntime {
97
+ port: number;
98
+ ownerToken: string;
99
+ nonOwnerToken?: string;
100
+ }
101
+
102
+ let _mcpLoopback: { port: number; token: string } | undefined;
103
+ let _mcpBootstrapAttempted = false;
104
+
105
+ function getEnvMcpLoopback(): { port: number; token: string } | undefined {
106
+ const portRaw = process.env.__GLUECLAW_MCP_PORT;
98
107
  const token = process.env.__GLUECLAW_MCP_TOKEN;
99
- if (port && token) return { port: parseInt(port, 10), token };
108
+ if (!portRaw || !token) return undefined;
109
+
110
+ const port = Number.parseInt(portRaw, 10);
111
+ if (!Number.isFinite(port) || port <= 0) return undefined;
112
+ return { port, token };
113
+ }
114
+
115
+ function openClawDistFromNodePath(nodePath: string): string | undefined {
116
+ const normalized = normalize(nodePath);
117
+ if (!normalized.includes("openclaw")) return undefined;
118
+ if (basename(normalized) !== "node_modules") return undefined;
119
+ return join(dirname(normalized), "dist");
120
+ }
121
+
122
+ export function resetMcpLoopbackForTests(): void {
123
+ _mcpLoopback = undefined;
124
+ _mcpBootstrapAttempted = false;
125
+ }
126
+
127
+ /** Bootstrap OpenClaw's MCP loopback server in-process and return the
128
+ * port + owner token. GlueClaw runs inside the gateway process, so we
129
+ * share OpenClaw's module cache: importing the same `mcp-http-*.js`
130
+ * the gateway loaded gives us the singleton, and a no-op when another
131
+ * caller already started it.
132
+ *
133
+ * Returns undefined if the OpenClaw dist cannot be located or its API
134
+ * has changed — in that case the claude subprocess simply runs without
135
+ * session tools, matching pre-RFC-001 behavior. */
136
+ export async function getMcpLoopback(): Promise<
137
+ { port: number; token: string } | undefined
138
+ > {
139
+ const envLoopback = getEnvMcpLoopback();
140
+ if (envLoopback) return envLoopback;
141
+
142
+ if (_mcpLoopback) return _mcpLoopback;
143
+ if (_mcpBootstrapAttempted) return undefined;
144
+ _mcpBootstrapAttempted = true;
145
+
146
+ try {
147
+ const { readdir } = await import("node:fs/promises");
148
+ const nodePaths = (process.env.NODE_PATH ?? "").split(delimiter);
149
+ const distDirs = nodePaths
150
+ .map(openClawDistFromNodePath)
151
+ .filter((p): p is string => Boolean(p));
152
+
153
+ for (const distDir of distDirs) {
154
+ try {
155
+ const files = await readdir(distDir);
156
+ const mcpFile = files.find(
157
+ (f) => f.startsWith("mcp-http-") && f.endsWith(".js"),
158
+ );
159
+ if (!mcpFile) continue;
160
+ const mod = (await import(
161
+ pathToFileURL(join(distDir, mcpFile)).href
162
+ )) as Record<string, unknown>;
163
+ // Minified aliases: n=ensureMcpLoopbackServer, i=getActiveMcpLoopbackRuntime
164
+ const ensureFn = (mod["n"] ?? mod["ensureMcpLoopbackServer"]) as
165
+ | (() => Promise<unknown>)
166
+ | undefined;
167
+ const getRuntime = (mod["i"] ?? mod["getActiveMcpLoopbackRuntime"]) as
168
+ | (() => McpLoopbackRuntime | undefined)
169
+ | undefined;
170
+ if (
171
+ typeof ensureFn !== "function" ||
172
+ typeof getRuntime !== "function"
173
+ ) {
174
+ continue;
175
+ }
176
+ await ensureFn();
177
+ const runtime = getRuntime();
178
+ if (runtime?.port && runtime.ownerToken) {
179
+ _mcpLoopback = { port: runtime.port, token: runtime.ownerToken };
180
+ return _mcpLoopback;
181
+ }
182
+ } catch {
183
+ continue;
184
+ }
185
+ }
186
+ } catch {
187
+ // Non-fatal: session tools simply won't be available
188
+ }
100
189
  return undefined;
101
190
  }
102
191
 
@@ -184,6 +273,15 @@ export function createClaudeCliStreamFn(opts: {
184
273
  let mcpCleanup: (() => void) | undefined;
185
274
  let stderrBuf = "";
186
275
  try {
276
+ const turnSessionKey = deriveTurnSessionKey({
277
+ agentId: opts.agentId,
278
+ systemPrompt: context.systemPrompt,
279
+ messages: context.messages as
280
+ | Array<{ role: string; content: unknown }>
281
+ | undefined,
282
+ });
283
+ const effectiveSessionKey =
284
+ turnSessionKey ?? opts.sessionKey ?? "default";
187
285
  // Scrub Anthropic detection triggers (see docs/detection-patterns.md)
188
286
  const cleanPrompt = scrubPrompt(context.systemPrompt ?? "");
189
287
  const resolvedModel = opts.modelOverride ?? model.id;
@@ -200,7 +298,7 @@ export function createClaudeCliStreamFn(opts: {
200
298
  // otherwise stick to whatever identity was used on the first turn,
201
299
  // leaving no way for callers to reinforce or correct an agent's
202
300
  // identity across turns.
203
- const sessionKey = `glueclaw:${opts.sessionKey ?? "default"}`;
301
+ const sessionKey = `glueclaw:${effectiveSessionKey}`;
204
302
  const existingSessionId = sessionMap.get(sessionKey);
205
303
  if (existingSessionId) {
206
304
  args.push("--resume", existingSessionId);
@@ -230,13 +328,13 @@ export function createClaudeCliStreamFn(opts: {
230
328
  delete env.ANTHROPIC_API_KEY_OLD;
231
329
 
232
330
  // Wire up MCP bridge for OpenClaw gateway tools
233
- const loopback = getMcpLoopback();
331
+ const loopback = await getMcpLoopback();
234
332
  if (loopback) {
235
333
  const mcp = writeMcpConfig(loopback.port);
236
334
  mcpCleanup = mcp.cleanup;
237
335
  args.push("--strict-mcp-config", "--mcp-config", mcp.path);
238
336
  env.OPENCLAW_MCP_TOKEN = loopback.token;
239
- env.OPENCLAW_MCP_SESSION_KEY = opts.sessionKey ?? "";
337
+ env.OPENCLAW_MCP_SESSION_KEY = effectiveSessionKey;
240
338
  env.OPENCLAW_MCP_AGENT_ID = opts.agentId ?? "main";
241
339
  env.OPENCLAW_MCP_ACCOUNT_ID = "";
242
340
  env.OPENCLAW_MCP_MESSAGE_CHANNEL = "";