@blockrun/clawrouter 0.12.229 → 0.12.230

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/cli.js CHANGED
@@ -82181,6 +82181,65 @@ function extractGptCalls(content) {
82181
82181
  }
82182
82182
  return { calls: [], matches: [] };
82183
82183
  }
82184
+ function deriveRequestTools(tools) {
82185
+ if (!Array.isArray(tools)) return [];
82186
+ const derived = [];
82187
+ for (const raw of tools) {
82188
+ const fn = raw?.function;
82189
+ const name = fn?.name;
82190
+ if (typeof name !== "string" || name.trim() === "") continue;
82191
+ const params = fn?.parameters;
82192
+ const props = isPlainObject(params?.properties) ? Object.keys(params.properties) : [];
82193
+ const required = Array.isArray(params?.required) ? params.required.filter((r2) => typeof r2 === "string") : [];
82194
+ derived.push({ name: name.trim(), propKeys: new Set(props), required });
82195
+ }
82196
+ return derived;
82197
+ }
82198
+ function keyMatchesParam(key2, propKeys) {
82199
+ if (propKeys.has(key2)) return true;
82200
+ if (key2 === "cmd" && propKeys.has("command")) return true;
82201
+ if (key2 === "command" && propKeys.has("cmd")) return true;
82202
+ return false;
82203
+ }
82204
+ function requiredSatisfied(tool, blobKeys) {
82205
+ return tool.required.every(
82206
+ (r2) => blobKeys.has(r2) || r2 === "command" && blobKeys.has("cmd") || r2 === "cmd" && blobKeys.has("command")
82207
+ );
82208
+ }
82209
+ function resolveKimiTool(prose, blob, tools) {
82210
+ const blobKeys = Object.keys(blob);
82211
+ if (prose) {
82212
+ const named = tools.filter(
82213
+ (t) => new RegExp(`(?:^|[^A-Za-z0-9_])${escapeRegExp2(t.name)}(?:[^A-Za-z0-9_]|$)`).test(prose)
82214
+ );
82215
+ if (named.length === 1) return named[0].name;
82216
+ }
82217
+ if (blobKeys.length === 0) return null;
82218
+ const keySet = new Set(blobKeys);
82219
+ const sigMatches = tools.filter(
82220
+ (t) => blobKeys.every((k) => keyMatchesParam(k, t.propKeys)) && requiredSatisfied(t, keySet)
82221
+ );
82222
+ return sigMatches.length === 1 ? sigMatches[0].name : null;
82223
+ }
82224
+ function escapeRegExp2(s3) {
82225
+ return s3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82226
+ }
82227
+ function extractKimiCalls(content, tools) {
82228
+ if (tools.length === 0) return { calls: [], matches: [] };
82229
+ const trailing = findTrailingJsonObject(content);
82230
+ if (!trailing || !isPlainObject(trailing.parsed)) return { calls: [], matches: [] };
82231
+ if (typeof trailing.parsed.name === "string" && trailing.parsed.name.trim() !== "") {
82232
+ return { calls: [], matches: [] };
82233
+ }
82234
+ const prose = content.slice(0, trailing.start).trim();
82235
+ const name = resolveKimiTool(prose, trailing.parsed, tools);
82236
+ if (!name) return { calls: [], matches: [] };
82237
+ const start = prose === "" ? 0 : trailing.start;
82238
+ return {
82239
+ calls: [makeCall(name, normalizeGptArgs(trailing.parsed))],
82240
+ matches: [{ start, end: content.length }]
82241
+ };
82242
+ }
82184
82243
  function stripRanges(content, ranges) {
82185
82244
  if (ranges.length === 0) return content;
82186
82245
  const sorted = [...ranges].sort((a, b) => a.start - b.start);
@@ -82195,15 +82254,23 @@ function stripRanges(content, ranges) {
82195
82254
  cleaned += content.slice(cursor);
82196
82255
  return cleaned;
82197
82256
  }
82198
- function extractTextualToolCalls(content) {
82257
+ function extractTextualToolCalls(content, options) {
82199
82258
  if (!content) {
82200
82259
  return { toolCalls: [], cleanedContent: "" };
82201
82260
  }
82202
82261
  const openClaw = extractOpenClawCalls(content);
82203
82262
  const anthropic = extractAnthropicCalls(content);
82204
82263
  const gemini = extractGeminiCalls(content);
82205
- const gpt = openClaw.calls.length + anthropic.calls.length + gemini.calls.length === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82206
- const toolCalls = [...openClaw.calls, ...anthropic.calls, ...gemini.calls, ...gpt.calls];
82264
+ const tagBasedCount = openClaw.calls.length + anthropic.calls.length + gemini.calls.length;
82265
+ const gpt = tagBasedCount === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82266
+ const kimi = tagBasedCount + gpt.calls.length === 0 ? extractKimiCalls(content, deriveRequestTools(options?.tools)) : { calls: [], matches: [] };
82267
+ const toolCalls = [
82268
+ ...openClaw.calls,
82269
+ ...anthropic.calls,
82270
+ ...gemini.calls,
82271
+ ...gpt.calls,
82272
+ ...kimi.calls
82273
+ ];
82207
82274
  if (toolCalls.length === 0) {
82208
82275
  return { toolCalls: [], cleanedContent: content };
82209
82276
  }
@@ -82211,7 +82278,8 @@ function extractTextualToolCalls(content) {
82211
82278
  ...openClaw.matches,
82212
82279
  ...anthropic.matches,
82213
82280
  ...gemini.matches,
82214
- ...gpt.matches
82281
+ ...gpt.matches,
82282
+ ...kimi.matches
82215
82283
  ]);
82216
82284
  return { toolCalls, cleanedContent };
82217
82285
  }
@@ -85986,6 +86054,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
85986
86054
  const debugMode = debugHeadersEnabledFromEnv() && req.headers["x-clawrouter-debug"] !== "false";
85987
86055
  let routingDecision;
85988
86056
  let hasTools = false;
86057
+ let requestTools;
85989
86058
  let hasVision = false;
85990
86059
  let isStreaming = false;
85991
86060
  let modelId = "";
@@ -86023,6 +86092,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
86023
86092
  requestSummaryForStore = summarizeRequest(promptText);
86024
86093
  }
86025
86094
  hasTools = Array.isArray(parsed.tools) && parsed.tools.length > 0;
86095
+ requestTools = parsed.tools;
86026
86096
  const rawLastContent = lastUserMsg?.content;
86027
86097
  const lastContent = typeof rawLastContent === "string" ? rawLastContent : Array.isArray(rawLastContent) ? rawLastContent.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
86028
86098
  if (sessionId && parsedMessages.length > 0) {
@@ -87566,7 +87636,7 @@ data: [DONE]
87566
87636
  let toolCalls = choice.message?.tool_calls ?? choice.delta?.tool_calls;
87567
87637
  const rawContent = choice.message?.content ?? choice.delta?.content ?? "";
87568
87638
  if (!endsWithToolCalls && (!toolCalls || toolCalls.length === 0) && rawContent) {
87569
- const extracted = extractTextualToolCalls(rawContent);
87639
+ const extracted = extractTextualToolCalls(rawContent, { tools: requestTools });
87570
87640
  if (extracted.toolCalls.length > 0) {
87571
87641
  toolCalls = extracted.toolCalls;
87572
87642
  }
@@ -87779,7 +87849,7 @@ data: [DONE]
87779
87849
  }
87780
87850
  continue;
87781
87851
  }
87782
- const extracted = extractTextualToolCalls(message.content);
87852
+ const extracted = extractTextualToolCalls(message.content, { tools: requestTools });
87783
87853
  if (extracted.toolCalls.length > 0) {
87784
87854
  message.tool_calls = extracted.toolCalls;
87785
87855
  message.content = "";
package/dist/index.js CHANGED
@@ -82001,6 +82001,65 @@ function extractGptCalls(content) {
82001
82001
  }
82002
82002
  return { calls: [], matches: [] };
82003
82003
  }
82004
+ function deriveRequestTools(tools) {
82005
+ if (!Array.isArray(tools)) return [];
82006
+ const derived = [];
82007
+ for (const raw of tools) {
82008
+ const fn = raw?.function;
82009
+ const name = fn?.name;
82010
+ if (typeof name !== "string" || name.trim() === "") continue;
82011
+ const params = fn?.parameters;
82012
+ const props = isPlainObject(params?.properties) ? Object.keys(params.properties) : [];
82013
+ const required = Array.isArray(params?.required) ? params.required.filter((r2) => typeof r2 === "string") : [];
82014
+ derived.push({ name: name.trim(), propKeys: new Set(props), required });
82015
+ }
82016
+ return derived;
82017
+ }
82018
+ function keyMatchesParam(key2, propKeys) {
82019
+ if (propKeys.has(key2)) return true;
82020
+ if (key2 === "cmd" && propKeys.has("command")) return true;
82021
+ if (key2 === "command" && propKeys.has("cmd")) return true;
82022
+ return false;
82023
+ }
82024
+ function requiredSatisfied(tool, blobKeys) {
82025
+ return tool.required.every(
82026
+ (r2) => blobKeys.has(r2) || r2 === "command" && blobKeys.has("cmd") || r2 === "cmd" && blobKeys.has("command")
82027
+ );
82028
+ }
82029
+ function resolveKimiTool(prose, blob, tools) {
82030
+ const blobKeys = Object.keys(blob);
82031
+ if (prose) {
82032
+ const named = tools.filter(
82033
+ (t) => new RegExp(`(?:^|[^A-Za-z0-9_])${escapeRegExp2(t.name)}(?:[^A-Za-z0-9_]|$)`).test(prose)
82034
+ );
82035
+ if (named.length === 1) return named[0].name;
82036
+ }
82037
+ if (blobKeys.length === 0) return null;
82038
+ const keySet = new Set(blobKeys);
82039
+ const sigMatches = tools.filter(
82040
+ (t) => blobKeys.every((k) => keyMatchesParam(k, t.propKeys)) && requiredSatisfied(t, keySet)
82041
+ );
82042
+ return sigMatches.length === 1 ? sigMatches[0].name : null;
82043
+ }
82044
+ function escapeRegExp2(s3) {
82045
+ return s3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82046
+ }
82047
+ function extractKimiCalls(content, tools) {
82048
+ if (tools.length === 0) return { calls: [], matches: [] };
82049
+ const trailing = findTrailingJsonObject(content);
82050
+ if (!trailing || !isPlainObject(trailing.parsed)) return { calls: [], matches: [] };
82051
+ if (typeof trailing.parsed.name === "string" && trailing.parsed.name.trim() !== "") {
82052
+ return { calls: [], matches: [] };
82053
+ }
82054
+ const prose = content.slice(0, trailing.start).trim();
82055
+ const name = resolveKimiTool(prose, trailing.parsed, tools);
82056
+ if (!name) return { calls: [], matches: [] };
82057
+ const start = prose === "" ? 0 : trailing.start;
82058
+ return {
82059
+ calls: [makeCall(name, normalizeGptArgs(trailing.parsed))],
82060
+ matches: [{ start, end: content.length }]
82061
+ };
82062
+ }
82004
82063
  function stripRanges(content, ranges) {
82005
82064
  if (ranges.length === 0) return content;
82006
82065
  const sorted = [...ranges].sort((a, b) => a.start - b.start);
@@ -82015,15 +82074,23 @@ function stripRanges(content, ranges) {
82015
82074
  cleaned += content.slice(cursor);
82016
82075
  return cleaned;
82017
82076
  }
82018
- function extractTextualToolCalls(content) {
82077
+ function extractTextualToolCalls(content, options) {
82019
82078
  if (!content) {
82020
82079
  return { toolCalls: [], cleanedContent: "" };
82021
82080
  }
82022
82081
  const openClaw = extractOpenClawCalls(content);
82023
82082
  const anthropic = extractAnthropicCalls(content);
82024
82083
  const gemini = extractGeminiCalls(content);
82025
- const gpt = openClaw.calls.length + anthropic.calls.length + gemini.calls.length === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82026
- const toolCalls = [...openClaw.calls, ...anthropic.calls, ...gemini.calls, ...gpt.calls];
82084
+ const tagBasedCount = openClaw.calls.length + anthropic.calls.length + gemini.calls.length;
82085
+ const gpt = tagBasedCount === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82086
+ const kimi = tagBasedCount + gpt.calls.length === 0 ? extractKimiCalls(content, deriveRequestTools(options?.tools)) : { calls: [], matches: [] };
82087
+ const toolCalls = [
82088
+ ...openClaw.calls,
82089
+ ...anthropic.calls,
82090
+ ...gemini.calls,
82091
+ ...gpt.calls,
82092
+ ...kimi.calls
82093
+ ];
82027
82094
  if (toolCalls.length === 0) {
82028
82095
  return { toolCalls: [], cleanedContent: content };
82029
82096
  }
@@ -82031,7 +82098,8 @@ function extractTextualToolCalls(content) {
82031
82098
  ...openClaw.matches,
82032
82099
  ...anthropic.matches,
82033
82100
  ...gemini.matches,
82034
- ...gpt.matches
82101
+ ...gpt.matches,
82102
+ ...kimi.matches
82035
82103
  ]);
82036
82104
  return { toolCalls, cleanedContent };
82037
82105
  }
@@ -85806,6 +85874,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
85806
85874
  const debugMode = debugHeadersEnabledFromEnv() && req.headers["x-clawrouter-debug"] !== "false";
85807
85875
  let routingDecision;
85808
85876
  let hasTools = false;
85877
+ let requestTools;
85809
85878
  let hasVision = false;
85810
85879
  let isStreaming = false;
85811
85880
  let modelId = "";
@@ -85843,6 +85912,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
85843
85912
  requestSummaryForStore = summarizeRequest(promptText);
85844
85913
  }
85845
85914
  hasTools = Array.isArray(parsed.tools) && parsed.tools.length > 0;
85915
+ requestTools = parsed.tools;
85846
85916
  const rawLastContent = lastUserMsg?.content;
85847
85917
  const lastContent = typeof rawLastContent === "string" ? rawLastContent : Array.isArray(rawLastContent) ? rawLastContent.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
85848
85918
  if (sessionId && parsedMessages.length > 0) {
@@ -87386,7 +87456,7 @@ data: [DONE]
87386
87456
  let toolCalls = choice.message?.tool_calls ?? choice.delta?.tool_calls;
87387
87457
  const rawContent = choice.message?.content ?? choice.delta?.content ?? "";
87388
87458
  if (!endsWithToolCalls && (!toolCalls || toolCalls.length === 0) && rawContent) {
87389
- const extracted = extractTextualToolCalls(rawContent);
87459
+ const extracted = extractTextualToolCalls(rawContent, { tools: requestTools });
87390
87460
  if (extracted.toolCalls.length > 0) {
87391
87461
  toolCalls = extracted.toolCalls;
87392
87462
  }
@@ -87599,7 +87669,7 @@ data: [DONE]
87599
87669
  }
87600
87670
  continue;
87601
87671
  }
87602
- const extracted = extractTextualToolCalls(message.content);
87672
+ const extracted = extractTextualToolCalls(message.content, { tools: requestTools });
87603
87673
  if (extracted.toolCalls.length > 0) {
87604
87674
  message.tool_calls = extracted.toolCalls;
87605
87675
  message.content = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.229",
3
+ "version": "0.12.230",
4
4
  "description": "Smart LLM router — save 85% on inference costs. 55+ models (8 free), one wallet, x402 micropayments.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/scripts/update.sh CHANGED
@@ -16,6 +16,96 @@ WALLET_FILE="$HOME/.openclaw/blockrun/wallet.key"
16
16
  WALLET_BACKUP=""
17
17
  PLUGIN_BACKUP=""
18
18
  CONFIG_BACKUP=""
19
+ ACTIVE_PACKAGE_DIR=""
20
+
21
+ NPM_PREFIX="$(npm config get prefix 2>/dev/null || echo "")"
22
+ if [ -n "$NPM_PREFIX" ] && [ -d "$NPM_PREFIX/bin" ]; then
23
+ PATH="$NPM_PREFIX/bin:$PATH"
24
+ export PATH
25
+ fi
26
+
27
+ find_clawrouter_package_dir() {
28
+ node <<'NODE'
29
+ const fs = require('fs');
30
+ const os = require('os');
31
+ const path = require('path');
32
+
33
+ const home = os.homedir();
34
+ const candidates = [
35
+ { packagePath: path.join(home, '.openclaw', 'extensions', 'clawrouter', 'package.json'), priority: 0 },
36
+ { packagePath: path.join(home, '.openclaw', 'npm', 'node_modules', '@blockrun', 'clawrouter', 'package.json'), priority: 2 },
37
+ ];
38
+
39
+ const projectsDir = path.join(home, '.openclaw', 'npm', 'projects');
40
+ try {
41
+ for (const name of fs.readdirSync(projectsDir)) {
42
+ candidates.push({
43
+ packagePath: path.join(projectsDir, name, 'node_modules', '@blockrun', 'clawrouter', 'package.json'),
44
+ priority: 1,
45
+ });
46
+ }
47
+ } catch {}
48
+
49
+ function versionParts(version) {
50
+ return String(version || '').split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
51
+ }
52
+
53
+ function compareVersions(a, b) {
54
+ const left = versionParts(a);
55
+ const right = versionParts(b);
56
+ for (let i = 0; i < 3; i++) {
57
+ if (left[i] !== right[i]) return left[i] - right[i];
58
+ }
59
+ return 0;
60
+ }
61
+
62
+ let best = null;
63
+ for (const candidate of candidates) {
64
+ try {
65
+ const stat = fs.statSync(candidate.packagePath);
66
+ const pkg = JSON.parse(fs.readFileSync(candidate.packagePath, 'utf8'));
67
+ if (pkg?.name !== '@blockrun/clawrouter') continue;
68
+ const versionOrder = best ? compareVersions(pkg.version, best.pkg.version) : 1;
69
+ if (
70
+ !best ||
71
+ versionOrder > 0 ||
72
+ (versionOrder === 0 &&
73
+ (candidate.priority < best.priority ||
74
+ (candidate.priority === best.priority && stat.mtimeMs > best.mtimeMs)))
75
+ ) {
76
+ best = { ...candidate, pkg, mtimeMs: stat.mtimeMs };
77
+ }
78
+ } catch {}
79
+ }
80
+
81
+ if (best) console.log(path.dirname(best.packagePath));
82
+ NODE
83
+ }
84
+
85
+ sync_cli_shim() {
86
+ local package_dir="$1"
87
+ if [ -z "$package_dir" ] || [ ! -f "$package_dir/dist/cli.js" ]; then
88
+ echo " ⚠ Could not update CLI shim: ClawRouter CLI not found"
89
+ return 1
90
+ fi
91
+
92
+ local prefix
93
+ prefix=$(npm config get prefix 2>/dev/null || echo "")
94
+ if [ -z "$prefix" ]; then
95
+ echo " ⚠ Could not update CLI shim: npm prefix unavailable"
96
+ return 1
97
+ fi
98
+
99
+ local bin_dir="$prefix/bin"
100
+ mkdir -p "$bin_dir"
101
+ rm -f "$bin_dir/clawrouter"
102
+ cat >"$bin_dir/clawrouter" <<EOF
103
+ #!/bin/sh
104
+ exec node "$package_dir/dist/cli.js" "\$@"
105
+ EOF
106
+ chmod +x "$bin_dir/clawrouter"
107
+ echo " ✓ CLI shim updated: $bin_dir/clawrouter → $package_dir/dist/cli.js"
108
+ }
19
109
 
20
110
  cleanup_backups() {
21
111
  if [ -n "$PLUGIN_BACKUP" ] && [ -d "$PLUGIN_BACKUP" ]; then
@@ -93,6 +183,7 @@ echo ""
93
183
 
94
184
  # Pre-flight: fail fast if config is corrupt
95
185
  validate_config
186
+ ACTIVE_PACKAGE_DIR=$(find_clawrouter_package_dir)
96
187
 
97
188
  echo "→ Checking wallet..."
98
189
 
@@ -105,7 +196,7 @@ if [ -f "$WALLET_FILE" ]; then
105
196
  # Derive wallet address via node (viem is available post-install)
106
197
  WALLET_ADDRESS=$(node -e "
107
198
  try {
108
- const { privateKeyToAccount } = require('$HOME/.openclaw/extensions/clawrouter/node_modules/viem/accounts/index.js');
199
+ const { privateKeyToAccount } = require(require.resolve('viem/accounts', { paths: ['$ACTIVE_PACKAGE_DIR'] }));
109
200
  const acct = privateKeyToAccount('$WALLET_KEY');
110
201
  console.log(acct.address);
111
202
  } catch {
@@ -551,41 +642,42 @@ force_install_from_npm() {
551
642
  return 1
552
643
  }
553
644
 
554
- if [ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ]; then
555
- LATEST_VER=$(npm view @blockrun/clawrouter@latest version 2>/dev/null || echo "")
556
- if [ -z "$LATEST_VER" ]; then
557
- echo " ✗ Could not resolve latest ClawRouter version from npm"
558
- exit 1
645
+ LATEST_VER=$(npm view @blockrun/clawrouter@latest version 2>/dev/null || echo "")
646
+ if [ -z "$LATEST_VER" ]; then
647
+ echo " ✗ Could not resolve latest ClawRouter version from npm"
648
+ exit 1
649
+ fi
650
+
651
+ ACTIVE_PACKAGE_DIR=$(find_clawrouter_package_dir)
652
+ INSTALLED_VER=""
653
+ if [ -n "$ACTIVE_PACKAGE_DIR" ] && [ -f "$ACTIVE_PACKAGE_DIR/package.json" ]; then
654
+ INSTALLED_VER=$(node -e "try{const p=require('$ACTIVE_PACKAGE_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
655
+ fi
656
+
657
+ if [ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ] || [ -z "$INSTALLED_VER" ] || [ "$INSTALLED_VER" != "$LATEST_VER" ]; then
658
+ if [ -n "$INSTALLED_VER" ] && [ "$INSTALLED_VER" != "$LATEST_VER" ]; then
659
+ echo " ⚠️ OpenClaw installed v${INSTALLED_VER} but latest is v${LATEST_VER}"
559
660
  fi
560
661
  force_install_from_npm "$LATEST_VER"
561
- if [ ! -f "$PLUGIN_DIR/package.json" ]; then
562
- echo " ✗ ClawRouter package.json missing after npm fallback"
563
- exit 1
564
- fi
565
- INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
566
- if [ -z "$INSTALLED_VER" ]; then
567
- echo " ✗ Could not verify ClawRouter version after npm fallback"
568
- exit 1
569
- fi
570
- echo " ✓ ClawRouter v${INSTALLED_VER} installed"
571
- trap - EXIT INT TERM
572
- elif [ -d "$PLUGIN_DIR" ] && [ -f "$PLUGIN_DIR/package.json" ]; then
573
- INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
574
- LATEST_VER=$(npm view @blockrun/clawrouter@latest version 2>/dev/null || echo "")
575
- if [ -n "$LATEST_VER" ] && [ -n "$INSTALLED_VER" ] && [ "$INSTALLED_VER" != "$LATEST_VER" ]; then
576
- echo " ⚠️ openclaw installed v${INSTALLED_VER} (cached) but latest is v${LATEST_VER}"
577
- force_install_from_npm "$LATEST_VER" || true
578
- fi
579
- INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('?');}" 2>/dev/null || echo "?")
580
- echo " ✓ ClawRouter v${INSTALLED_VER} installed"
662
+ ACTIVE_PACKAGE_DIR=$(find_clawrouter_package_dir)
663
+ INSTALLED_VER=$(node -e "try{const p=require('$ACTIVE_PACKAGE_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
581
664
  fi
582
665
 
666
+ if [ -z "$ACTIVE_PACKAGE_DIR" ] || [ -z "$INSTALLED_VER" ]; then
667
+ echo " ✗ Could not verify ClawRouter install"
668
+ exit 1
669
+ fi
670
+
671
+ echo " ✓ ClawRouter v${INSTALLED_VER} installed at $ACTIVE_PACKAGE_DIR"
672
+ sync_cli_shim "$ACTIVE_PACKAGE_DIR"
673
+ trap - EXIT INT TERM
674
+
583
675
  # ── Step 4c: Ensure all dependencies are installed ────────────
584
676
  # openclaw's plugin installer may skip native/optional deps like @solana/kit.
585
677
  # Run npm install in the plugin directory to fill any gaps.
586
- if [ -d "$PLUGIN_DIR" ] && [ -f "$PLUGIN_DIR/package.json" ]; then
678
+ if [ -n "$ACTIVE_PACKAGE_DIR" ] && [ -f "$ACTIVE_PACKAGE_DIR/package.json" ]; then
587
679
  echo "→ Installing dependencies (Solana, x402, etc.)..."
588
- run_dependency_install "$PLUGIN_DIR"
680
+ run_dependency_install "$ACTIVE_PACKAGE_DIR"
589
681
  fi
590
682
 
591
683
  # ── Step 5: Verify wallet survived ─────────────────────────────
@@ -724,7 +816,7 @@ const os = require('os');
724
816
  const fs = require('fs');
725
817
  const path = require('path');
726
818
  const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
727
- const topModelsPath = '$SCRIPT_DIR/../src/top-models.json';
819
+ const topModelsPath = path.join('$ACTIVE_PACKAGE_DIR', 'src', 'top-models.json');
728
820
 
729
821
  if (!fs.existsSync(configPath)) {
730
822
  console.log(' No config file found, skipping');
@@ -1025,7 +1117,7 @@ if [ -f "$WALLET_FILE" ]; then
1025
1117
  FINAL_KEY=$(cat "$WALLET_FILE" | tr -d '[:space:]')
1026
1118
  FINAL_ADDRESS=$(node -e "
1027
1119
  try {
1028
- const { privateKeyToAccount } = require('$HOME/.openclaw/extensions/clawrouter/node_modules/viem/accounts/index.js');
1120
+ const { privateKeyToAccount } = require(require.resolve('viem/accounts', { paths: ['$ACTIVE_PACKAGE_DIR'] }));
1029
1121
  console.log(privateKeyToAccount('$FINAL_KEY').address);
1030
1122
  } catch { console.log('(run /wallet in OpenClaw to see your address)'); }
1031
1123
  " 2>/dev/null || echo "(run /wallet in OpenClaw to see your address)")