@chrysb/alphaclaw 0.8.7-beta.1 → 0.8.7-beta.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/bin/alphaclaw.js CHANGED
@@ -16,6 +16,11 @@ const {
16
16
  const {
17
17
  applyPendingAlphaclawUpdate,
18
18
  } = require("../lib/server/pending-alphaclaw-update");
19
+ const {
20
+ getManagedAlphaclawCliPath,
21
+ getManagedAlphaclawRuntimeDir,
22
+ syncManagedAlphaclawRuntimeWithBundled,
23
+ } = require("../lib/server/alphaclaw-runtime");
19
24
  const {
20
25
  applyPendingOpenclawUpdate,
21
26
  } = require("../lib/server/pending-openclaw-update");
@@ -24,14 +29,10 @@ const {
24
29
  prependManagedOpenclawBinToPath,
25
30
  syncManagedOpenclawRuntimeWithBundled,
26
31
  } = require("../lib/server/openclaw-runtime");
27
-
28
- const kUsageTrackerPluginPath = path.resolve(
29
- __dirname,
30
- "..",
31
- "lib",
32
- "plugin",
33
- "usage-tracker",
34
- );
32
+ const {
33
+ ensurePluginsShell,
34
+ ensureUsageTrackerPluginEntry,
35
+ } = require("../lib/server/usage-tracker-config");
35
36
 
36
37
  // ---------------------------------------------------------------------------
37
38
  // Parse CLI flags
@@ -173,10 +174,89 @@ if (portFlag) {
173
174
  process.env.PORT = portFlag;
174
175
  }
175
176
 
177
+ const kManagedAlphaclawRuntimeEnvFlag = "ALPHACLAW_MANAGED_RUNTIME_ACTIVE";
178
+ const shouldBootstrapManagedAlphaclawRuntime =
179
+ command === "start" &&
180
+ process.env[kManagedAlphaclawRuntimeEnvFlag] !== "1";
181
+
176
182
  // ---------------------------------------------------------------------------
177
183
  // 2. Create directory structure
178
184
  // ---------------------------------------------------------------------------
179
185
 
186
+ if (shouldBootstrapManagedAlphaclawRuntime) {
187
+ const { spawn } = require("child_process");
188
+ const managedAlphaclawRuntimeDir = getManagedAlphaclawRuntimeDir({ rootDir });
189
+ const pendingUpdateMarker = path.join(rootDir, ".alphaclaw-update-pending");
190
+ if (fs.existsSync(pendingUpdateMarker)) {
191
+ applyPendingAlphaclawUpdate({
192
+ execSyncImpl: execSync,
193
+ fsModule: fs,
194
+ installDir: managedAlphaclawRuntimeDir,
195
+ logger: console,
196
+ markerPath: pendingUpdateMarker,
197
+ });
198
+ }
199
+ try {
200
+ syncManagedAlphaclawRuntimeWithBundled({
201
+ execSyncImpl: execSync,
202
+ fsModule: fs,
203
+ logger: console,
204
+ runtimeDir: managedAlphaclawRuntimeDir,
205
+ });
206
+ } catch (error) {
207
+ console.log(
208
+ `[alphaclaw] Could not sync managed AlphaClaw runtime from bundled install: ${error.message}`,
209
+ );
210
+ }
211
+
212
+ const managedAlphaclawCliPath = getManagedAlphaclawCliPath({
213
+ runtimeDir: managedAlphaclawRuntimeDir,
214
+ });
215
+ if (!fs.existsSync(managedAlphaclawCliPath)) {
216
+ console.error(
217
+ `[alphaclaw] Managed AlphaClaw runtime missing CLI at ${managedAlphaclawCliPath}`,
218
+ );
219
+ process.exit(1);
220
+ }
221
+
222
+ const runtimeChild = spawn(
223
+ process.argv[0],
224
+ [managedAlphaclawCliPath, ...process.argv.slice(2)],
225
+ {
226
+ stdio: "inherit",
227
+ env: {
228
+ ...process.env,
229
+ [kManagedAlphaclawRuntimeEnvFlag]: "1",
230
+ ALPHACLAW_BOOTSTRAP_CLI_PATH: __filename,
231
+ },
232
+ },
233
+ );
234
+
235
+ const forwardSignal = (signal) => {
236
+ if (runtimeChild.exitCode === null && !runtimeChild.killed) {
237
+ runtimeChild.kill(signal);
238
+ }
239
+ };
240
+
241
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
242
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
243
+
244
+ runtimeChild.on("error", (error) => {
245
+ console.error(
246
+ `[alphaclaw] Managed AlphaClaw runtime launch failed: ${error.message}`,
247
+ );
248
+ process.exit(1);
249
+ });
250
+
251
+ runtimeChild.on("exit", (code, signal) => {
252
+ if (signal) {
253
+ process.kill(process.pid, signal);
254
+ return;
255
+ }
256
+ process.exit(Number.isInteger(code) ? code : 0);
257
+ });
258
+ } else {
259
+
180
260
  const openclawDir = path.join(rootDir, ".openclaw");
181
261
  fs.mkdirSync(openclawDir, { recursive: true });
182
262
  const { hourlyGitSyncPath } = migrateManagedInternalFiles({
@@ -210,36 +290,6 @@ if (fs.existsSync(pendingUpdateMarker)) {
210
290
  }
211
291
  }
212
292
 
213
- const pendingOpenclawUpdateMarker = path.join(rootDir, ".openclaw-update-pending");
214
- const managedOpenclawRuntimeDir = getManagedOpenclawRuntimeDir({ rootDir });
215
- if (fs.existsSync(pendingOpenclawUpdateMarker)) {
216
- applyPendingOpenclawUpdate({
217
- execSyncImpl: execSync,
218
- fsModule: fs,
219
- installDir: managedOpenclawRuntimeDir,
220
- logger: console,
221
- markerPath: pendingOpenclawUpdateMarker,
222
- });
223
- }
224
- try {
225
- syncManagedOpenclawRuntimeWithBundled({
226
- execSyncImpl: execSync,
227
- fsModule: fs,
228
- logger: console,
229
- runtimeDir: managedOpenclawRuntimeDir,
230
- });
231
- } catch (error) {
232
- console.log(
233
- `[alphaclaw] Could not sync managed OpenClaw runtime from bundled install: ${error.message}`,
234
- );
235
- }
236
- prependManagedOpenclawBinToPath({
237
- env: process.env,
238
- fsModule: fs,
239
- logger: console,
240
- runtimeDir: managedOpenclawRuntimeDir,
241
- });
242
-
243
293
  // ---------------------------------------------------------------------------
244
294
  // 3. Symlink ~/.openclaw -> <root>/.openclaw
245
295
  // ---------------------------------------------------------------------------
@@ -546,7 +596,41 @@ if (!kSetupPassword) {
546
596
  }
547
597
 
548
598
  // ---------------------------------------------------------------------------
549
- // 7. Set OPENCLAW_HOME globally so all child processes inherit it
599
+ // 7. Prepare managed OpenClaw runtime
600
+ // ---------------------------------------------------------------------------
601
+
602
+ const pendingOpenclawUpdateMarker = path.join(rootDir, ".openclaw-update-pending");
603
+ const managedOpenclawRuntimeDir = getManagedOpenclawRuntimeDir({ rootDir });
604
+ if (fs.existsSync(pendingOpenclawUpdateMarker)) {
605
+ applyPendingOpenclawUpdate({
606
+ execSyncImpl: execSync,
607
+ fsModule: fs,
608
+ installDir: managedOpenclawRuntimeDir,
609
+ logger: console,
610
+ markerPath: pendingOpenclawUpdateMarker,
611
+ });
612
+ }
613
+ try {
614
+ syncManagedOpenclawRuntimeWithBundled({
615
+ execSyncImpl: execSync,
616
+ fsModule: fs,
617
+ logger: console,
618
+ runtimeDir: managedOpenclawRuntimeDir,
619
+ });
620
+ } catch (error) {
621
+ console.log(
622
+ `[alphaclaw] Could not sync managed OpenClaw runtime from bundled install: ${error.message}`,
623
+ );
624
+ }
625
+ prependManagedOpenclawBinToPath({
626
+ env: process.env,
627
+ fsModule: fs,
628
+ logger: console,
629
+ runtimeDir: managedOpenclawRuntimeDir,
630
+ });
631
+
632
+ // ---------------------------------------------------------------------------
633
+ // 8. Set OPENCLAW_HOME globally so all child processes inherit it
550
634
  // ---------------------------------------------------------------------------
551
635
 
552
636
  process.env.OPENCLAW_HOME = rootDir;
@@ -555,7 +639,7 @@ process.env.GOG_KEYRING_PASSWORD =
555
639
  process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
556
640
 
557
641
  // ---------------------------------------------------------------------------
558
- // 8. Install gog (Google Workspace CLI) if not present
642
+ // 9. Install gog (Google Workspace CLI) if not present
559
643
  // ---------------------------------------------------------------------------
560
644
 
561
645
  process.env.XDG_CONFIG_HOME = openclawDir;
@@ -588,7 +672,7 @@ if (!gogInstalled) {
588
672
  }
589
673
 
590
674
  // ---------------------------------------------------------------------------
591
- // 9. Install/reconcile system cron entry
675
+ // 10. Install/reconcile system cron entry
592
676
  // ---------------------------------------------------------------------------
593
677
 
594
678
  const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
@@ -654,7 +738,7 @@ if (fs.existsSync(hourlyGitSyncPath)) {
654
738
  }
655
739
 
656
740
  // ---------------------------------------------------------------------------
657
- // 9. Start cron daemon if available
741
+ // 11. Start cron daemon if available
658
742
  // ---------------------------------------------------------------------------
659
743
 
660
744
  try {
@@ -668,7 +752,7 @@ try {
668
752
  } catch {}
669
753
 
670
754
  // ---------------------------------------------------------------------------
671
- // 10. Reconcile channels if already onboarded
755
+ // 12. Reconcile channels if already onboarded
672
756
  // ---------------------------------------------------------------------------
673
757
 
674
758
  const configPath = path.join(openclawDir, "openclaw.json");
@@ -816,10 +900,7 @@ if (fs.existsSync(configPath)) {
816
900
  try {
817
901
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
818
902
  if (!cfg.channels) cfg.channels = {};
819
- if (!cfg.plugins) cfg.plugins = {};
820
- if (!cfg.plugins.load) cfg.plugins.load = {};
821
- if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
822
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
903
+ ensurePluginsShell(cfg);
823
904
  let changed = false;
824
905
 
825
906
  if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
@@ -845,12 +926,7 @@ if (fs.existsSync(configPath)) {
845
926
  console.log("[alphaclaw] Discord added");
846
927
  changed = true;
847
928
  }
848
- if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
849
- cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
850
- changed = true;
851
- }
852
- if (cfg.plugins.entries["usage-tracker"]?.enabled !== true) {
853
- cfg.plugins.entries["usage-tracker"] = { enabled: true };
929
+ if (ensureUsageTrackerPluginEntry(cfg)) {
854
930
  changed = true;
855
931
  }
856
932
 
@@ -952,3 +1028,4 @@ try {
952
1028
 
953
1029
  console.log("[alphaclaw] Setup complete -- starting server");
954
1030
  require("../lib/server.js");
1031
+ }
@@ -0,0 +1,238 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const { kRootDir } = require("./constants");
5
+ const { compareVersionParts } = require("./helpers");
6
+ const {
7
+ computePackageFingerprint,
8
+ isPackageRootSymlink,
9
+ packLocalPackageForInstall,
10
+ } = require("./package-fingerprint");
11
+
12
+ const getManagedAlphaclawRuntimeDir = ({ rootDir = kRootDir } = {}) =>
13
+ path.join(rootDir, ".alphaclaw-runtime");
14
+
15
+ const getBundledAlphaclawPackageRoot = () => path.resolve(__dirname, "..", "..");
16
+
17
+ const getManagedAlphaclawPackageRoot = ({ runtimeDir } = {}) =>
18
+ path.join(
19
+ runtimeDir || getManagedAlphaclawRuntimeDir(),
20
+ "node_modules",
21
+ "@chrysb",
22
+ "alphaclaw",
23
+ );
24
+
25
+ const getManagedAlphaclawCliPath = ({ runtimeDir } = {}) =>
26
+ path.join(
27
+ getManagedAlphaclawPackageRoot({ runtimeDir }),
28
+ "bin",
29
+ "alphaclaw.js",
30
+ );
31
+
32
+ const getManagedAlphaclawPackageJsonPath = ({ runtimeDir } = {}) =>
33
+ path.join(
34
+ getManagedAlphaclawPackageRoot({ runtimeDir }),
35
+ "package.json",
36
+ );
37
+
38
+ const ensureManagedAlphaclawRuntimeProject = ({
39
+ fsModule = fs,
40
+ runtimeDir,
41
+ } = {}) => {
42
+ const resolvedRuntimeDir = runtimeDir || getManagedAlphaclawRuntimeDir();
43
+ const packageJsonPath = path.join(resolvedRuntimeDir, "package.json");
44
+ fsModule.mkdirSync(resolvedRuntimeDir, { recursive: true });
45
+ if (!fsModule.existsSync(packageJsonPath)) {
46
+ fsModule.writeFileSync(
47
+ packageJsonPath,
48
+ JSON.stringify(
49
+ {
50
+ name: "alphaclaw-runtime",
51
+ private: true,
52
+ },
53
+ null,
54
+ 2,
55
+ ),
56
+ );
57
+ }
58
+ return {
59
+ runtimeDir: resolvedRuntimeDir,
60
+ packageJsonPath,
61
+ };
62
+ };
63
+
64
+ const readManagedAlphaclawRuntimeVersion = ({
65
+ fsModule = fs,
66
+ runtimeDir,
67
+ } = {}) => {
68
+ try {
69
+ const pkg = JSON.parse(
70
+ fsModule.readFileSync(
71
+ getManagedAlphaclawPackageJsonPath({ runtimeDir }),
72
+ "utf8",
73
+ ),
74
+ );
75
+ return String(pkg?.version || "").trim() || null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ };
80
+
81
+ const readBundledAlphaclawVersion = ({
82
+ fsModule = fs,
83
+ packageJsonPath = path.resolve(__dirname, "..", "..", "package.json"),
84
+ } = {}) => {
85
+ try {
86
+ const pkg = JSON.parse(fsModule.readFileSync(packageJsonPath, "utf8"));
87
+ return String(pkg?.version || "").trim() || null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ };
92
+
93
+ const shellQuote = (value) =>
94
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
95
+
96
+ const installManagedAlphaclawRuntime = ({
97
+ execSyncImpl,
98
+ fsModule = fs,
99
+ runtimeDir,
100
+ spec,
101
+ sourcePath,
102
+ } = {}) => {
103
+ const normalizedSourcePath = String(sourcePath || "").trim();
104
+ const normalizedSpec = normalizedSourcePath
105
+ ? normalizedSourcePath
106
+ : String(spec || "").trim() || "@chrysb/alphaclaw@latest";
107
+ ensureManagedAlphaclawRuntimeProject({
108
+ fsModule,
109
+ runtimeDir,
110
+ });
111
+ let packedSource = null;
112
+ try {
113
+ const installTarget = normalizedSourcePath
114
+ ? (() => {
115
+ packedSource = packLocalPackageForInstall({
116
+ execSyncImpl,
117
+ fsModule,
118
+ packageRoot: normalizedSourcePath,
119
+ tempDirPrefix: "alphaclaw-runtime-pack-",
120
+ });
121
+ return packedSource.tarballPath;
122
+ })()
123
+ : normalizedSpec;
124
+ execSyncImpl(
125
+ `npm install ${shellQuote(installTarget)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
126
+ {
127
+ cwd: runtimeDir,
128
+ stdio: "inherit",
129
+ timeout: 180000,
130
+ },
131
+ );
132
+ } finally {
133
+ packedSource?.cleanup?.();
134
+ }
135
+ return {
136
+ spec: normalizedSpec,
137
+ version: readManagedAlphaclawRuntimeVersion({
138
+ fsModule,
139
+ runtimeDir,
140
+ }),
141
+ };
142
+ };
143
+
144
+ const syncManagedAlphaclawRuntimeWithBundled = ({
145
+ execSyncImpl,
146
+ fsModule = fs,
147
+ logger = console,
148
+ runtimeDir,
149
+ packageRoot = getBundledAlphaclawPackageRoot(),
150
+ packageJsonPath,
151
+ } = {}) => {
152
+ const bundledVersion = readBundledAlphaclawVersion({
153
+ fsModule,
154
+ packageJsonPath: packageJsonPath || path.join(packageRoot, "package.json"),
155
+ });
156
+ if (!bundledVersion) {
157
+ return {
158
+ checked: false,
159
+ synced: false,
160
+ bundledVersion: null,
161
+ runtimeVersion: readManagedAlphaclawRuntimeVersion({
162
+ fsModule,
163
+ runtimeDir,
164
+ }),
165
+ };
166
+ }
167
+
168
+ const runtimeVersion = readManagedAlphaclawRuntimeVersion({
169
+ fsModule,
170
+ runtimeDir,
171
+ });
172
+ const runtimePackageRoot = getManagedAlphaclawPackageRoot({ runtimeDir });
173
+ const runtimePackageRootIsSymlink = isPackageRootSymlink({
174
+ fsModule,
175
+ packageRoot: runtimePackageRoot,
176
+ });
177
+ const bundledFingerprint = computePackageFingerprint({
178
+ fsModule,
179
+ packageRoot,
180
+ packageJsonPath: packageJsonPath || path.join(packageRoot, "package.json"),
181
+ });
182
+ const runtimeFingerprint = computePackageFingerprint({
183
+ fsModule,
184
+ packageRoot: runtimePackageRoot,
185
+ packageJsonPath: getManagedAlphaclawPackageJsonPath({ runtimeDir }),
186
+ });
187
+ if (runtimeVersion && compareVersionParts(runtimeVersion, bundledVersion) >= 0) {
188
+ if (
189
+ compareVersionParts(runtimeVersion, bundledVersion) > 0 ||
190
+ (!runtimePackageRootIsSymlink &&
191
+ (!bundledFingerprint || runtimeFingerprint === bundledFingerprint))
192
+ ) {
193
+ return {
194
+ checked: true,
195
+ synced: false,
196
+ bundledVersion,
197
+ runtimeVersion,
198
+ };
199
+ }
200
+ logger.log(
201
+ runtimePackageRootIsSymlink
202
+ ? `[alphaclaw] Managed AlphaClaw runtime ${runtimeVersion} is symlinked to the bundled package; refreshing runtime...`
203
+ : `[alphaclaw] Managed AlphaClaw runtime ${runtimeVersion} differs from bundled ${bundledVersion}; refreshing runtime...`,
204
+ );
205
+ } else {
206
+ logger.log(
207
+ runtimeVersion
208
+ ? `[alphaclaw] Managed AlphaClaw runtime ${runtimeVersion} is older than bundled ${bundledVersion}; syncing runtime...`
209
+ : `[alphaclaw] Managed AlphaClaw runtime missing; seeding bundled AlphaClaw ${bundledVersion}...`,
210
+ );
211
+ }
212
+
213
+ const installResult = installManagedAlphaclawRuntime({
214
+ execSyncImpl,
215
+ fsModule,
216
+ runtimeDir,
217
+ sourcePath: packageRoot,
218
+ });
219
+ return {
220
+ checked: true,
221
+ synced: true,
222
+ bundledVersion,
223
+ runtimeVersion: installResult.version || bundledVersion,
224
+ };
225
+ };
226
+
227
+ module.exports = {
228
+ ensureManagedAlphaclawRuntimeProject,
229
+ getBundledAlphaclawPackageRoot,
230
+ getManagedAlphaclawCliPath,
231
+ getManagedAlphaclawPackageJsonPath,
232
+ getManagedAlphaclawPackageRoot,
233
+ getManagedAlphaclawRuntimeDir,
234
+ installManagedAlphaclawRuntime,
235
+ readBundledAlphaclawVersion,
236
+ readManagedAlphaclawRuntimeVersion,
237
+ syncManagedAlphaclawRuntimeWithBundled,
238
+ };
@@ -126,9 +126,15 @@ const createAlphaclawVersionService = () => {
126
126
  // On bare metal / Mac / Linux, spawn a replacement process then exit.
127
127
  console.log("[alphaclaw] Spawning new process and exiting...");
128
128
  const { spawn } = require("child_process");
129
- const child = spawn(process.argv[0], process.argv.slice(1), {
129
+ const nextEnv = { ...process.env };
130
+ delete nextEnv.ALPHACLAW_MANAGED_RUNTIME_ACTIVE;
131
+ const bootstrapCliPath =
132
+ String(process.env.ALPHACLAW_BOOTSTRAP_CLI_PATH || "").trim() ||
133
+ process.argv[1];
134
+ const child = spawn(process.argv[0], [bootstrapCliPath, ...process.argv.slice(2)], {
130
135
  detached: true,
131
136
  stdio: "inherit",
137
+ env: nextEnv,
132
138
  });
133
139
  child.unref();
134
140
  process.exit(0);
@@ -6,10 +6,32 @@ const {
6
6
  compareVersionParts,
7
7
  normalizeOpenclawVersion,
8
8
  } = require("./helpers");
9
+ const {
10
+ computePackageFingerprint,
11
+ isPackageRootSymlink,
12
+ packLocalPackageForInstall,
13
+ resolvePackageRootFromEntryPath,
14
+ } = require("./package-fingerprint");
9
15
 
10
16
  const getManagedOpenclawRuntimeDir = ({ rootDir = kRootDir } = {}) =>
11
17
  path.join(rootDir, ".openclaw-runtime");
12
18
 
19
+ const getBundledOpenclawPackageRoot = ({
20
+ fsModule = fs,
21
+ resolveImpl = require.resolve,
22
+ } = {}) =>
23
+ resolvePackageRootFromEntryPath({
24
+ fsModule,
25
+ entryPath: resolveImpl("openclaw"),
26
+ });
27
+
28
+ const getManagedOpenclawPackageRoot = ({ runtimeDir } = {}) =>
29
+ path.join(
30
+ runtimeDir || getManagedOpenclawRuntimeDir(),
31
+ "node_modules",
32
+ "openclaw",
33
+ );
34
+
13
35
  const getManagedOpenclawBinDir = ({ runtimeDir } = {}) =>
14
36
  path.join(
15
37
  runtimeDir || getManagedOpenclawRuntimeDir(),
@@ -22,9 +44,7 @@ const getManagedOpenclawBinPath = ({ runtimeDir } = {}) =>
22
44
 
23
45
  const getManagedOpenclawPackageJsonPath = ({ runtimeDir } = {}) =>
24
46
  path.join(
25
- runtimeDir || getManagedOpenclawRuntimeDir(),
26
- "node_modules",
27
- "openclaw",
47
+ getManagedOpenclawPackageRoot({ runtimeDir }),
28
48
  "package.json",
29
49
  );
30
50
 
@@ -76,8 +96,14 @@ const readBundledOpenclawVersion = ({
76
96
  resolveImpl = require.resolve,
77
97
  } = {}) => {
78
98
  try {
79
- const pkgPath = resolveImpl("openclaw/package.json");
80
- const pkg = JSON.parse(fsModule.readFileSync(pkgPath, "utf8"));
99
+ const packageRoot = getBundledOpenclawPackageRoot({
100
+ fsModule,
101
+ resolveImpl,
102
+ });
103
+ if (!packageRoot) return null;
104
+ const pkg = JSON.parse(
105
+ fsModule.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
106
+ );
81
107
  return normalizeOpenclawVersion(pkg?.version || "");
82
108
  } catch {
83
109
  return null;
@@ -136,21 +162,41 @@ const installManagedOpenclawRuntime = ({
136
162
  logger = console,
137
163
  runtimeDir,
138
164
  spec,
165
+ sourcePath,
139
166
  alphaclawRoot,
140
167
  } = {}) => {
141
- const normalizedSpec = String(spec || "").trim() || "openclaw@latest";
168
+ const normalizedSourcePath = String(sourcePath || "").trim();
169
+ const normalizedSpec = normalizedSourcePath
170
+ ? normalizedSourcePath
171
+ : String(spec || "").trim() || "openclaw@latest";
142
172
  ensureManagedOpenclawRuntimeProject({
143
173
  fsModule,
144
174
  runtimeDir,
145
175
  });
146
- execSyncImpl(
147
- `npm install ${shellQuote(normalizedSpec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
148
- {
149
- cwd: runtimeDir,
150
- stdio: "inherit",
151
- timeout: 180000,
152
- },
153
- );
176
+ let packedSource = null;
177
+ try {
178
+ const installTarget = normalizedSourcePath
179
+ ? (() => {
180
+ packedSource = packLocalPackageForInstall({
181
+ execSyncImpl,
182
+ fsModule,
183
+ packageRoot: normalizedSourcePath,
184
+ tempDirPrefix: "openclaw-runtime-pack-",
185
+ });
186
+ return packedSource.tarballPath;
187
+ })()
188
+ : normalizedSpec;
189
+ execSyncImpl(
190
+ `npm install ${shellQuote(installTarget)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
191
+ {
192
+ cwd: runtimeDir,
193
+ stdio: "inherit",
194
+ timeout: 180000,
195
+ },
196
+ );
197
+ } finally {
198
+ packedSource?.cleanup?.();
199
+ }
154
200
  const installedVersion = readManagedOpenclawRuntimeVersion({
155
201
  fsModule,
156
202
  runtimeDir,
@@ -177,6 +223,10 @@ const syncManagedOpenclawRuntimeWithBundled = ({
177
223
  resolveImpl,
178
224
  alphaclawRoot,
179
225
  } = {}) => {
226
+ const bundledPackageRoot = getBundledOpenclawPackageRoot({
227
+ fsModule,
228
+ resolveImpl,
229
+ });
180
230
  const bundledVersion = readBundledOpenclawVersion({
181
231
  fsModule,
182
232
  resolveImpl,
@@ -194,26 +244,52 @@ const syncManagedOpenclawRuntimeWithBundled = ({
194
244
  fsModule,
195
245
  runtimeDir,
196
246
  });
247
+ const runtimePackageRoot = getManagedOpenclawPackageRoot({ runtimeDir });
248
+ const runtimePackageRootIsSymlink = isPackageRootSymlink({
249
+ fsModule,
250
+ packageRoot: runtimePackageRoot,
251
+ });
252
+ const bundledFingerprint = computePackageFingerprint({
253
+ fsModule,
254
+ packageRoot: bundledPackageRoot,
255
+ });
256
+ const runtimeFingerprint = computePackageFingerprint({
257
+ fsModule,
258
+ packageRoot: runtimePackageRoot,
259
+ packageJsonPath: getManagedOpenclawPackageJsonPath({ runtimeDir }),
260
+ });
197
261
  if (runtimeVersion && compareVersionParts(runtimeVersion, bundledVersion) >= 0) {
198
- return {
199
- checked: true,
200
- synced: false,
201
- bundledVersion,
202
- runtimeVersion,
203
- };
262
+ if (
263
+ compareVersionParts(runtimeVersion, bundledVersion) > 0 ||
264
+ (!runtimePackageRootIsSymlink &&
265
+ (!bundledFingerprint || runtimeFingerprint === bundledFingerprint))
266
+ ) {
267
+ return {
268
+ checked: true,
269
+ synced: false,
270
+ bundledVersion,
271
+ runtimeVersion,
272
+ };
273
+ }
274
+ logger.log(
275
+ runtimePackageRootIsSymlink
276
+ ? `[alphaclaw] Managed OpenClaw runtime ${runtimeVersion} is symlinked to the bundled package; refreshing runtime...`
277
+ : `[alphaclaw] Managed OpenClaw runtime ${runtimeVersion} differs from bundled ${bundledVersion}; refreshing runtime...`,
278
+ );
279
+ } else {
280
+ logger.log(
281
+ runtimeVersion
282
+ ? `[alphaclaw] Managed OpenClaw runtime ${runtimeVersion} is older than bundled ${bundledVersion}; syncing runtime...`
283
+ : `[alphaclaw] Managed OpenClaw runtime missing; seeding bundled OpenClaw ${bundledVersion}...`,
284
+ );
204
285
  }
205
286
 
206
- logger.log(
207
- runtimeVersion
208
- ? `[alphaclaw] Managed OpenClaw runtime ${runtimeVersion} is older than bundled ${bundledVersion}; syncing runtime...`
209
- : `[alphaclaw] Managed OpenClaw runtime missing; seeding bundled OpenClaw ${bundledVersion}...`,
210
- );
211
287
  const installResult = installManagedOpenclawRuntime({
212
288
  execSyncImpl,
213
289
  fsModule,
214
290
  logger,
215
291
  runtimeDir,
216
- spec: `openclaw@${bundledVersion}`,
292
+ sourcePath: bundledPackageRoot,
217
293
  alphaclawRoot,
218
294
  });
219
295
  return {
@@ -248,8 +324,10 @@ const prependManagedOpenclawBinToPath = ({
248
324
  module.exports = {
249
325
  applyManagedOpenclawPatch,
250
326
  ensureManagedOpenclawRuntimeProject,
327
+ getBundledOpenclawPackageRoot,
251
328
  getManagedOpenclawBinDir,
252
329
  getManagedOpenclawBinPath,
330
+ getManagedOpenclawPackageRoot,
253
331
  getManagedOpenclawPackageJsonPath,
254
332
  getManagedOpenclawRuntimeDir,
255
333
  installManagedOpenclawRuntime,
@@ -0,0 +1,202 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ const kIgnoredDirectoryNames = new Set([".git", "node_modules"]);
7
+
8
+ const normalizeRelativePath = (packageRoot, absolutePath) =>
9
+ path.relative(packageRoot, absolutePath).split(path.sep).join("/");
10
+
11
+ const addIncludedPath = ({ includeSet, value }) => {
12
+ const normalizedValue = String(value || "").trim();
13
+ if (!normalizedValue) return;
14
+ includeSet.add(normalizedValue.replace(/\/+$/, ""));
15
+ };
16
+
17
+ const collectIncludedPaths = ({ packageJson = {} } = {}) => {
18
+ const includeSet = new Set(["package.json"]);
19
+
20
+ if (Array.isArray(packageJson.files)) {
21
+ for (const entry of packageJson.files) {
22
+ addIncludedPath({ includeSet, value: entry });
23
+ }
24
+ }
25
+
26
+ if (typeof packageJson.bin === "string") {
27
+ addIncludedPath({ includeSet, value: packageJson.bin });
28
+ } else if (packageJson.bin && typeof packageJson.bin === "object") {
29
+ for (const entry of Object.values(packageJson.bin)) {
30
+ addIncludedPath({ includeSet, value: entry });
31
+ }
32
+ }
33
+
34
+ return Array.from(includeSet).sort((left, right) => left.localeCompare(right));
35
+ };
36
+
37
+ const walkIncludedFiles = ({
38
+ fsModule = fs,
39
+ packageRoot,
40
+ absolutePath,
41
+ files,
42
+ }) => {
43
+ if (!fsModule.existsSync(absolutePath)) return;
44
+ const relativePath = normalizeRelativePath(packageRoot, absolutePath);
45
+ if (!relativePath || relativePath.startsWith("..")) return;
46
+
47
+ const stat = fsModule.lstatSync(absolutePath);
48
+ if (stat.isSymbolicLink()) {
49
+ files.push({
50
+ relativePath,
51
+ hash: `symlink:${fsModule.readlinkSync(absolutePath)}`,
52
+ });
53
+ return;
54
+ }
55
+ if (stat.isFile()) {
56
+ files.push({
57
+ relativePath,
58
+ hash: crypto
59
+ .createHash("sha256")
60
+ .update(fsModule.readFileSync(absolutePath))
61
+ .digest("hex"),
62
+ });
63
+ return;
64
+ }
65
+ if (!stat.isDirectory()) return;
66
+
67
+ const entries = fsModule
68
+ .readdirSync(absolutePath, { withFileTypes: true })
69
+ .sort((left, right) => left.name.localeCompare(right.name));
70
+
71
+ for (const entry of entries) {
72
+ if (entry.isDirectory() && kIgnoredDirectoryNames.has(entry.name)) continue;
73
+ walkIncludedFiles({
74
+ fsModule,
75
+ packageRoot,
76
+ absolutePath: path.join(absolutePath, entry.name),
77
+ files,
78
+ });
79
+ }
80
+ };
81
+
82
+ const computePackageFingerprint = ({
83
+ fsModule = fs,
84
+ packageRoot,
85
+ packageJsonPath = path.join(packageRoot, "package.json"),
86
+ } = {}) => {
87
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
88
+ if (!resolvedPackageRoot || !fsModule.existsSync(packageJsonPath)) return null;
89
+
90
+ let packageJson;
91
+ try {
92
+ packageJson = JSON.parse(fsModule.readFileSync(packageJsonPath, "utf8"));
93
+ } catch {
94
+ return null;
95
+ }
96
+
97
+ const files = [];
98
+ for (const includePath of collectIncludedPaths({ packageJson })) {
99
+ walkIncludedFiles({
100
+ fsModule,
101
+ packageRoot: resolvedPackageRoot,
102
+ absolutePath: path.resolve(resolvedPackageRoot, includePath),
103
+ files,
104
+ });
105
+ }
106
+
107
+ const hash = crypto.createHash("sha256");
108
+ hash.update("package-fingerprint-v1");
109
+ for (const entry of files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))) {
110
+ hash.update(entry.relativePath);
111
+ hash.update("\0");
112
+ hash.update(entry.hash);
113
+ hash.update("\0");
114
+ }
115
+ return hash.digest("hex");
116
+ };
117
+
118
+ const isPackageRootSymlink = ({
119
+ fsModule = fs,
120
+ packageRoot,
121
+ } = {}) => {
122
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
123
+ if (!resolvedPackageRoot || !fsModule.existsSync(resolvedPackageRoot)) return false;
124
+ try {
125
+ return fsModule.lstatSync(resolvedPackageRoot).isSymbolicLink();
126
+ } catch {
127
+ return false;
128
+ }
129
+ };
130
+
131
+ const resolvePackageRootFromEntryPath = ({
132
+ fsModule = fs,
133
+ entryPath,
134
+ } = {}) => {
135
+ let cursor = path.dirname(path.resolve(String(entryPath || "")));
136
+ while (cursor && cursor !== path.dirname(cursor)) {
137
+ if (fsModule.existsSync(path.join(cursor, "package.json"))) {
138
+ return cursor;
139
+ }
140
+ cursor = path.dirname(cursor);
141
+ }
142
+ return null;
143
+ };
144
+
145
+ const packLocalPackageForInstall = ({
146
+ execSyncImpl,
147
+ fsModule = fs,
148
+ packageRoot,
149
+ tempDirPrefix = "alphaclaw-package-pack-",
150
+ } = {}) => {
151
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
152
+ const packDir = fsModule.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
153
+ try {
154
+ const packStdout = String(
155
+ execSyncImpl(
156
+ `npm pack ${shellQuote(resolvedPackageRoot)} --quiet --ignore-scripts --pack-destination ${shellQuote(packDir)}`,
157
+ {
158
+ encoding: "utf8",
159
+ stdio: ["ignore", "pipe", "inherit"],
160
+ timeout: 180000,
161
+ },
162
+ ) || "",
163
+ )
164
+ .trim()
165
+ .split(/\r?\n/)
166
+ .map((entry) => entry.trim())
167
+ .filter(Boolean);
168
+ const packFileName =
169
+ packStdout.at(-1) ||
170
+ fsModule.readdirSync(packDir).find((entry) => entry.endsWith(".tgz"));
171
+ if (!packFileName) {
172
+ throw new Error(`npm pack did not produce a tarball for ${resolvedPackageRoot}`);
173
+ }
174
+ const tarballPath = path.join(packDir, packFileName);
175
+ if (!fsModule.existsSync(tarballPath)) {
176
+ throw new Error(`Packed tarball missing at ${tarballPath}`);
177
+ }
178
+ return {
179
+ tarballPath,
180
+ cleanup: () => {
181
+ try {
182
+ fsModule.rmSync(packDir, { recursive: true, force: true });
183
+ } catch {}
184
+ },
185
+ };
186
+ } catch (error) {
187
+ try {
188
+ fsModule.rmSync(packDir, { recursive: true, force: true });
189
+ } catch {}
190
+ throw error;
191
+ }
192
+ };
193
+
194
+ const shellQuote = (value) =>
195
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
196
+
197
+ module.exports = {
198
+ computePackageFingerprint,
199
+ isPackageRootSymlink,
200
+ packLocalPackageForInstall,
201
+ resolvePackageRootFromEntryPath,
202
+ };
@@ -1,3 +1,7 @@
1
+ const {
2
+ installManagedAlphaclawRuntime,
3
+ } = require("./alphaclaw-runtime");
4
+
1
5
  const buildPendingAlphaclawInstallSpec = (marker = {}) => {
2
6
  const explicitSpec = String(marker?.spec || "").trim();
3
7
  if (explicitSpec) {
@@ -7,9 +11,6 @@ const buildPendingAlphaclawInstallSpec = (marker = {}) => {
7
11
  return `@chrysb/alphaclaw@${targetVersion}`;
8
12
  };
9
13
 
10
- const shellQuote = (value) =>
11
- `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
12
-
13
14
  const applyPendingAlphaclawUpdate = ({
14
15
  execSyncImpl,
15
16
  fsModule,
@@ -36,14 +37,12 @@ const applyPendingAlphaclawUpdate = ({
36
37
  logger.log(`[alphaclaw] Pending update detected, installing ${spec}...`);
37
38
 
38
39
  try {
39
- execSyncImpl(
40
- `npm install ${shellQuote(spec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
41
- {
42
- cwd: installDir,
43
- stdio: "inherit",
44
- timeout: 180000,
45
- },
46
- );
40
+ installManagedAlphaclawRuntime({
41
+ execSyncImpl,
42
+ fsModule,
43
+ runtimeDir: installDir,
44
+ spec,
45
+ });
47
46
  fsModule.unlinkSync(markerPath);
48
47
  logger.log("[alphaclaw] Update applied successfully");
49
48
  return {
@@ -8,6 +8,27 @@ const kUsageTrackerPluginPath = path.resolve(
8
8
  "usage-tracker",
9
9
  );
10
10
 
11
+ const normalizePluginPath = (value = "") =>
12
+ String(value || "")
13
+ .trim()
14
+ .replace(/\\/g, "/")
15
+ .replace(/\/+$/, "");
16
+
17
+ const isUsageTrackerPluginPath = (value = "") => {
18
+ const normalizedValue = normalizePluginPath(value);
19
+ const normalizedCanonicalPath = normalizePluginPath(kUsageTrackerPluginPath);
20
+ if (!normalizedValue) return false;
21
+ if (
22
+ normalizedValue === normalizedCanonicalPath ||
23
+ normalizedValue.startsWith(`${normalizedCanonicalPath}/`)
24
+ ) {
25
+ return true;
26
+ }
27
+ return /(?:^|\/)(?:node_modules\/@chrysb\/alphaclaw\/lib|lib)\/plugin\/usage-tracker(?:\/.*)?$/.test(
28
+ normalizedValue,
29
+ );
30
+ };
31
+
11
32
  const ensurePluginsShell = (cfg = {}) => {
12
33
  if (!cfg.plugins || typeof cfg.plugins !== "object") cfg.plugins = {};
13
34
  if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
@@ -32,9 +53,11 @@ const ensurePluginAllowed = ({ cfg = {}, pluginKey = "" }) => {
32
53
  const ensureUsageTrackerPluginEntry = (cfg = {}) => {
33
54
  const before = JSON.stringify(cfg);
34
55
  ensurePluginAllowed({ cfg, pluginKey: "usage-tracker" });
35
- if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
36
- cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
37
- }
56
+ const nextPaths = cfg.plugins.load.paths.filter(
57
+ (entry) => !isUsageTrackerPluginPath(entry),
58
+ );
59
+ nextPaths.push(kUsageTrackerPluginPath);
60
+ cfg.plugins.load.paths = nextPaths;
38
61
  cfg.plugins.entries["usage-tracker"] = {
39
62
  ...(cfg.plugins.entries["usage-tracker"] &&
40
63
  typeof cfg.plugins.entries["usage-tracker"] === "object"
@@ -64,6 +87,7 @@ const ensureUsageTrackerPluginConfig = ({ fsModule, openclawDir }) => {
64
87
 
65
88
  module.exports = {
66
89
  kUsageTrackerPluginPath,
90
+ isUsageTrackerPluginPath,
67
91
  ensurePluginsShell,
68
92
  ensurePluginAllowed,
69
93
  ensureUsageTrackerPluginEntry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.7-beta.1",
3
+ "version": "0.8.7-beta.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },