@chrysb/alphaclaw 0.8.7-beta.2 → 0.8.7-beta.4

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
@@ -29,14 +29,10 @@ const {
29
29
  prependManagedOpenclawBinToPath,
30
30
  syncManagedOpenclawRuntimeWithBundled,
31
31
  } = require("../lib/server/openclaw-runtime");
32
-
33
- const kUsageTrackerPluginPath = path.resolve(
34
- __dirname,
35
- "..",
36
- "lib",
37
- "plugin",
38
- "usage-tracker",
39
- );
32
+ const {
33
+ ensurePluginsShell,
34
+ ensureUsageTrackerPluginEntry,
35
+ } = require("../lib/server/usage-tracker-config");
40
36
 
41
37
  // ---------------------------------------------------------------------------
42
38
  // Parse CLI flags
@@ -294,36 +290,6 @@ if (fs.existsSync(pendingUpdateMarker)) {
294
290
  }
295
291
  }
296
292
 
297
- const pendingOpenclawUpdateMarker = path.join(rootDir, ".openclaw-update-pending");
298
- const managedOpenclawRuntimeDir = getManagedOpenclawRuntimeDir({ rootDir });
299
- if (fs.existsSync(pendingOpenclawUpdateMarker)) {
300
- applyPendingOpenclawUpdate({
301
- execSyncImpl: execSync,
302
- fsModule: fs,
303
- installDir: managedOpenclawRuntimeDir,
304
- logger: console,
305
- markerPath: pendingOpenclawUpdateMarker,
306
- });
307
- }
308
- try {
309
- syncManagedOpenclawRuntimeWithBundled({
310
- execSyncImpl: execSync,
311
- fsModule: fs,
312
- logger: console,
313
- runtimeDir: managedOpenclawRuntimeDir,
314
- });
315
- } catch (error) {
316
- console.log(
317
- `[alphaclaw] Could not sync managed OpenClaw runtime from bundled install: ${error.message}`,
318
- );
319
- }
320
- prependManagedOpenclawBinToPath({
321
- env: process.env,
322
- fsModule: fs,
323
- logger: console,
324
- runtimeDir: managedOpenclawRuntimeDir,
325
- });
326
-
327
293
  // ---------------------------------------------------------------------------
328
294
  // 3. Symlink ~/.openclaw -> <root>/.openclaw
329
295
  // ---------------------------------------------------------------------------
@@ -630,7 +596,41 @@ if (!kSetupPassword) {
630
596
  }
631
597
 
632
598
  // ---------------------------------------------------------------------------
633
- // 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
634
634
  // ---------------------------------------------------------------------------
635
635
 
636
636
  process.env.OPENCLAW_HOME = rootDir;
@@ -639,7 +639,7 @@ process.env.GOG_KEYRING_PASSWORD =
639
639
  process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
640
640
 
641
641
  // ---------------------------------------------------------------------------
642
- // 8. Install gog (Google Workspace CLI) if not present
642
+ // 9. Install gog (Google Workspace CLI) if not present
643
643
  // ---------------------------------------------------------------------------
644
644
 
645
645
  process.env.XDG_CONFIG_HOME = openclawDir;
@@ -672,7 +672,7 @@ if (!gogInstalled) {
672
672
  }
673
673
 
674
674
  // ---------------------------------------------------------------------------
675
- // 9. Install/reconcile system cron entry
675
+ // 10. Install/reconcile system cron entry
676
676
  // ---------------------------------------------------------------------------
677
677
 
678
678
  const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
@@ -738,7 +738,7 @@ if (fs.existsSync(hourlyGitSyncPath)) {
738
738
  }
739
739
 
740
740
  // ---------------------------------------------------------------------------
741
- // 9. Start cron daemon if available
741
+ // 11. Start cron daemon if available
742
742
  // ---------------------------------------------------------------------------
743
743
 
744
744
  try {
@@ -752,7 +752,7 @@ try {
752
752
  } catch {}
753
753
 
754
754
  // ---------------------------------------------------------------------------
755
- // 10. Reconcile channels if already onboarded
755
+ // 12. Reconcile channels if already onboarded
756
756
  // ---------------------------------------------------------------------------
757
757
 
758
758
  const configPath = path.join(openclawDir, "openclaw.json");
@@ -900,10 +900,7 @@ if (fs.existsSync(configPath)) {
900
900
  try {
901
901
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
902
902
  if (!cfg.channels) cfg.channels = {};
903
- if (!cfg.plugins) cfg.plugins = {};
904
- if (!cfg.plugins.load) cfg.plugins.load = {};
905
- if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
906
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
903
+ ensurePluginsShell(cfg);
907
904
  let changed = false;
908
905
 
909
906
  if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
@@ -929,12 +926,7 @@ if (fs.existsSync(configPath)) {
929
926
  console.log("[alphaclaw] Discord added");
930
927
  changed = true;
931
928
  }
932
- if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
933
- cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
934
- changed = true;
935
- }
936
- if (cfg.plugins.entries["usage-tracker"]?.enabled !== true) {
937
- cfg.plugins.entries["usage-tracker"] = { enabled: true };
929
+ if (ensureUsageTrackerPluginEntry(cfg)) {
938
930
  changed = true;
939
931
  }
940
932
 
@@ -3,26 +3,36 @@ const path = require("path");
3
3
 
4
4
  const { kRootDir } = require("./constants");
5
5
  const { compareVersionParts } = require("./helpers");
6
+ const {
7
+ computePackageFingerprint,
8
+ isPackageRootSymlink,
9
+ packLocalPackageForInstall,
10
+ seedRuntimeFromBundledInstall,
11
+ } = require("./package-fingerprint");
6
12
 
7
13
  const getManagedAlphaclawRuntimeDir = ({ rootDir = kRootDir } = {}) =>
8
14
  path.join(rootDir, ".alphaclaw-runtime");
9
15
 
10
- const getManagedAlphaclawCliPath = ({ runtimeDir } = {}) =>
16
+ const getBundledAlphaclawPackageRoot = () => path.resolve(__dirname, "..", "..");
17
+
18
+ const getManagedAlphaclawPackageRoot = ({ runtimeDir } = {}) =>
11
19
  path.join(
12
20
  runtimeDir || getManagedAlphaclawRuntimeDir(),
13
21
  "node_modules",
14
22
  "@chrysb",
15
23
  "alphaclaw",
24
+ );
25
+
26
+ const getManagedAlphaclawCliPath = ({ runtimeDir } = {}) =>
27
+ path.join(
28
+ getManagedAlphaclawPackageRoot({ runtimeDir }),
16
29
  "bin",
17
30
  "alphaclaw.js",
18
31
  );
19
32
 
20
33
  const getManagedAlphaclawPackageJsonPath = ({ runtimeDir } = {}) =>
21
34
  path.join(
22
- runtimeDir || getManagedAlphaclawRuntimeDir(),
23
- "node_modules",
24
- "@chrysb",
25
- "alphaclaw",
35
+ getManagedAlphaclawPackageRoot({ runtimeDir }),
26
36
  "package.json",
27
37
  );
28
38
 
@@ -89,21 +99,40 @@ const installManagedAlphaclawRuntime = ({
89
99
  fsModule = fs,
90
100
  runtimeDir,
91
101
  spec,
102
+ sourcePath,
92
103
  } = {}) => {
93
- const normalizedSpec =
94
- String(spec || "").trim() || "@chrysb/alphaclaw@latest";
104
+ const normalizedSourcePath = String(sourcePath || "").trim();
105
+ const normalizedSpec = normalizedSourcePath
106
+ ? normalizedSourcePath
107
+ : String(spec || "").trim() || "@chrysb/alphaclaw@latest";
95
108
  ensureManagedAlphaclawRuntimeProject({
96
109
  fsModule,
97
110
  runtimeDir,
98
111
  });
99
- execSyncImpl(
100
- `npm install ${shellQuote(normalizedSpec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
101
- {
102
- cwd: runtimeDir,
103
- stdio: "inherit",
104
- timeout: 180000,
105
- },
106
- );
112
+ let packedSource = null;
113
+ try {
114
+ const installTarget = normalizedSourcePath
115
+ ? (() => {
116
+ packedSource = packLocalPackageForInstall({
117
+ execSyncImpl,
118
+ fsModule,
119
+ packageRoot: normalizedSourcePath,
120
+ tempDirPrefix: "alphaclaw-runtime-pack-",
121
+ });
122
+ return packedSource.tarballPath;
123
+ })()
124
+ : normalizedSpec;
125
+ execSyncImpl(
126
+ `npm install ${shellQuote(installTarget)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
127
+ {
128
+ cwd: runtimeDir,
129
+ stdio: "inherit",
130
+ timeout: 180000,
131
+ },
132
+ );
133
+ } finally {
134
+ packedSource?.cleanup?.();
135
+ }
107
136
  return {
108
137
  spec: normalizedSpec,
109
138
  version: readManagedAlphaclawRuntimeVersion({
@@ -113,16 +142,48 @@ const installManagedAlphaclawRuntime = ({
113
142
  };
114
143
  };
115
144
 
145
+ const seedManagedAlphaclawRuntimeFromBundledInstall = ({
146
+ fsModule = fs,
147
+ logger = console,
148
+ runtimeDir,
149
+ packageRoot,
150
+ } = {}) => {
151
+ const seedResult = seedRuntimeFromBundledInstall({
152
+ fsModule,
153
+ packageRoot,
154
+ runtimeDir,
155
+ runtimePackageJson: {
156
+ name: "alphaclaw-runtime",
157
+ private: true,
158
+ },
159
+ });
160
+ if (!seedResult.seeded) {
161
+ return {
162
+ seeded: false,
163
+ version: null,
164
+ };
165
+ }
166
+ logger.log("[alphaclaw] Seeded managed AlphaClaw runtime from bundled node_modules");
167
+ return {
168
+ seeded: true,
169
+ version: readManagedAlphaclawRuntimeVersion({
170
+ fsModule,
171
+ runtimeDir,
172
+ }),
173
+ };
174
+ };
175
+
116
176
  const syncManagedAlphaclawRuntimeWithBundled = ({
117
177
  execSyncImpl,
118
178
  fsModule = fs,
119
179
  logger = console,
120
180
  runtimeDir,
181
+ packageRoot = getBundledAlphaclawPackageRoot(),
121
182
  packageJsonPath,
122
183
  } = {}) => {
123
184
  const bundledVersion = readBundledAlphaclawVersion({
124
185
  fsModule,
125
- packageJsonPath,
186
+ packageJsonPath: packageJsonPath || path.join(packageRoot, "package.json"),
126
187
  });
127
188
  if (!bundledVersion) {
128
189
  return {
@@ -140,25 +201,75 @@ const syncManagedAlphaclawRuntimeWithBundled = ({
140
201
  fsModule,
141
202
  runtimeDir,
142
203
  });
204
+ const runtimePackageRoot = getManagedAlphaclawPackageRoot({ runtimeDir });
205
+ const runtimePackageRootIsSymlink = isPackageRootSymlink({
206
+ fsModule,
207
+ packageRoot: runtimePackageRoot,
208
+ });
209
+ const bundledFingerprint = computePackageFingerprint({
210
+ fsModule,
211
+ packageRoot,
212
+ packageJsonPath: packageJsonPath || path.join(packageRoot, "package.json"),
213
+ });
214
+ const runtimeFingerprint = computePackageFingerprint({
215
+ fsModule,
216
+ packageRoot: runtimePackageRoot,
217
+ packageJsonPath: getManagedAlphaclawPackageJsonPath({ runtimeDir }),
218
+ });
143
219
  if (runtimeVersion && compareVersionParts(runtimeVersion, bundledVersion) >= 0) {
144
- return {
145
- checked: true,
146
- synced: false,
147
- bundledVersion,
148
- runtimeVersion,
149
- };
220
+ if (
221
+ compareVersionParts(runtimeVersion, bundledVersion) > 0 ||
222
+ (!runtimePackageRootIsSymlink &&
223
+ (!bundledFingerprint || runtimeFingerprint === bundledFingerprint))
224
+ ) {
225
+ return {
226
+ checked: true,
227
+ synced: false,
228
+ bundledVersion,
229
+ runtimeVersion,
230
+ };
231
+ }
232
+ logger.log(
233
+ runtimePackageRootIsSymlink
234
+ ? `[alphaclaw] Managed AlphaClaw runtime ${runtimeVersion} is symlinked to the bundled package; refreshing runtime...`
235
+ : `[alphaclaw] Managed AlphaClaw runtime ${runtimeVersion} differs from bundled ${bundledVersion}; refreshing runtime...`,
236
+ );
237
+ } else {
238
+ logger.log(
239
+ runtimeVersion
240
+ ? `[alphaclaw] Managed AlphaClaw runtime ${runtimeVersion} is older than bundled ${bundledVersion}; syncing runtime...`
241
+ : `[alphaclaw] Managed AlphaClaw runtime missing; seeding bundled AlphaClaw ${bundledVersion}...`,
242
+ );
243
+ }
244
+
245
+ if (!runtimeVersion) {
246
+ try {
247
+ const seedResult = seedManagedAlphaclawRuntimeFromBundledInstall({
248
+ fsModule,
249
+ logger,
250
+ runtimeDir,
251
+ packageRoot,
252
+ });
253
+ if (seedResult.seeded) {
254
+ return {
255
+ checked: true,
256
+ synced: true,
257
+ bundledVersion,
258
+ runtimeVersion: seedResult.version || bundledVersion,
259
+ };
260
+ }
261
+ } catch (error) {
262
+ logger.log(
263
+ `[alphaclaw] Could not seed managed AlphaClaw runtime from bundled node_modules: ${error.message}`,
264
+ );
265
+ }
150
266
  }
151
267
 
152
- logger.log(
153
- runtimeVersion
154
- ? `[alphaclaw] Managed AlphaClaw runtime ${runtimeVersion} is older than bundled ${bundledVersion}; syncing runtime...`
155
- : `[alphaclaw] Managed AlphaClaw runtime missing; seeding bundled AlphaClaw ${bundledVersion}...`,
156
- );
157
268
  const installResult = installManagedAlphaclawRuntime({
158
269
  execSyncImpl,
159
270
  fsModule,
160
271
  runtimeDir,
161
- spec: `@chrysb/alphaclaw@${bundledVersion}`,
272
+ sourcePath: packageRoot,
162
273
  });
163
274
  return {
164
275
  checked: true,
@@ -170,11 +281,14 @@ const syncManagedAlphaclawRuntimeWithBundled = ({
170
281
 
171
282
  module.exports = {
172
283
  ensureManagedAlphaclawRuntimeProject,
284
+ getBundledAlphaclawPackageRoot,
173
285
  getManagedAlphaclawCliPath,
174
286
  getManagedAlphaclawPackageJsonPath,
287
+ getManagedAlphaclawPackageRoot,
175
288
  getManagedAlphaclawRuntimeDir,
176
289
  installManagedAlphaclawRuntime,
177
290
  readBundledAlphaclawVersion,
178
291
  readManagedAlphaclawRuntimeVersion,
292
+ seedManagedAlphaclawRuntimeFromBundledInstall,
179
293
  syncManagedAlphaclawRuntimeWithBundled,
180
294
  };
@@ -6,10 +6,33 @@ const {
6
6
  compareVersionParts,
7
7
  normalizeOpenclawVersion,
8
8
  } = require("./helpers");
9
+ const {
10
+ computePackageFingerprint,
11
+ isPackageRootSymlink,
12
+ packLocalPackageForInstall,
13
+ resolvePackageRootFromEntryPath,
14
+ seedRuntimeFromBundledInstall,
15
+ } = require("./package-fingerprint");
9
16
 
10
17
  const getManagedOpenclawRuntimeDir = ({ rootDir = kRootDir } = {}) =>
11
18
  path.join(rootDir, ".openclaw-runtime");
12
19
 
20
+ const getBundledOpenclawPackageRoot = ({
21
+ fsModule = fs,
22
+ resolveImpl = require.resolve,
23
+ } = {}) =>
24
+ resolvePackageRootFromEntryPath({
25
+ fsModule,
26
+ entryPath: resolveImpl("openclaw"),
27
+ });
28
+
29
+ const getManagedOpenclawPackageRoot = ({ runtimeDir } = {}) =>
30
+ path.join(
31
+ runtimeDir || getManagedOpenclawRuntimeDir(),
32
+ "node_modules",
33
+ "openclaw",
34
+ );
35
+
13
36
  const getManagedOpenclawBinDir = ({ runtimeDir } = {}) =>
14
37
  path.join(
15
38
  runtimeDir || getManagedOpenclawRuntimeDir(),
@@ -22,9 +45,7 @@ const getManagedOpenclawBinPath = ({ runtimeDir } = {}) =>
22
45
 
23
46
  const getManagedOpenclawPackageJsonPath = ({ runtimeDir } = {}) =>
24
47
  path.join(
25
- runtimeDir || getManagedOpenclawRuntimeDir(),
26
- "node_modules",
27
- "openclaw",
48
+ getManagedOpenclawPackageRoot({ runtimeDir }),
28
49
  "package.json",
29
50
  );
30
51
 
@@ -76,8 +97,14 @@ const readBundledOpenclawVersion = ({
76
97
  resolveImpl = require.resolve,
77
98
  } = {}) => {
78
99
  try {
79
- const pkgPath = resolveImpl("openclaw/package.json");
80
- const pkg = JSON.parse(fsModule.readFileSync(pkgPath, "utf8"));
100
+ const packageRoot = getBundledOpenclawPackageRoot({
101
+ fsModule,
102
+ resolveImpl,
103
+ });
104
+ if (!packageRoot) return null;
105
+ const pkg = JSON.parse(
106
+ fsModule.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
107
+ );
81
108
  return normalizeOpenclawVersion(pkg?.version || "");
82
109
  } catch {
83
110
  return null;
@@ -136,21 +163,41 @@ const installManagedOpenclawRuntime = ({
136
163
  logger = console,
137
164
  runtimeDir,
138
165
  spec,
166
+ sourcePath,
139
167
  alphaclawRoot,
140
168
  } = {}) => {
141
- const normalizedSpec = String(spec || "").trim() || "openclaw@latest";
169
+ const normalizedSourcePath = String(sourcePath || "").trim();
170
+ const normalizedSpec = normalizedSourcePath
171
+ ? normalizedSourcePath
172
+ : String(spec || "").trim() || "openclaw@latest";
142
173
  ensureManagedOpenclawRuntimeProject({
143
174
  fsModule,
144
175
  runtimeDir,
145
176
  });
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
- );
177
+ let packedSource = null;
178
+ try {
179
+ const installTarget = normalizedSourcePath
180
+ ? (() => {
181
+ packedSource = packLocalPackageForInstall({
182
+ execSyncImpl,
183
+ fsModule,
184
+ packageRoot: normalizedSourcePath,
185
+ tempDirPrefix: "openclaw-runtime-pack-",
186
+ });
187
+ return packedSource.tarballPath;
188
+ })()
189
+ : normalizedSpec;
190
+ execSyncImpl(
191
+ `npm install ${shellQuote(installTarget)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
192
+ {
193
+ cwd: runtimeDir,
194
+ stdio: "inherit",
195
+ timeout: 180000,
196
+ },
197
+ );
198
+ } finally {
199
+ packedSource?.cleanup?.();
200
+ }
154
201
  const installedVersion = readManagedOpenclawRuntimeVersion({
155
202
  fsModule,
156
203
  runtimeDir,
@@ -169,6 +216,48 @@ const installManagedOpenclawRuntime = ({
169
216
  };
170
217
  };
171
218
 
219
+ const seedManagedOpenclawRuntimeFromBundledInstall = ({
220
+ execSyncImpl,
221
+ fsModule = fs,
222
+ logger = console,
223
+ runtimeDir,
224
+ bundledPackageRoot,
225
+ alphaclawRoot,
226
+ } = {}) => {
227
+ const seedResult = seedRuntimeFromBundledInstall({
228
+ fsModule,
229
+ packageRoot: bundledPackageRoot,
230
+ runtimeDir,
231
+ runtimePackageJson: {
232
+ name: "alphaclaw-openclaw-runtime",
233
+ private: true,
234
+ },
235
+ });
236
+ if (!seedResult.seeded) {
237
+ return {
238
+ seeded: false,
239
+ version: null,
240
+ };
241
+ }
242
+ const installedVersion = readManagedOpenclawRuntimeVersion({
243
+ fsModule,
244
+ runtimeDir,
245
+ });
246
+ applyManagedOpenclawPatch({
247
+ execSyncImpl,
248
+ fsModule,
249
+ logger,
250
+ runtimeDir,
251
+ version: installedVersion,
252
+ alphaclawRoot,
253
+ });
254
+ logger.log("[alphaclaw] Seeded managed OpenClaw runtime from bundled node_modules");
255
+ return {
256
+ seeded: true,
257
+ version: installedVersion,
258
+ };
259
+ };
260
+
172
261
  const syncManagedOpenclawRuntimeWithBundled = ({
173
262
  execSyncImpl,
174
263
  fsModule = fs,
@@ -177,6 +266,10 @@ const syncManagedOpenclawRuntimeWithBundled = ({
177
266
  resolveImpl,
178
267
  alphaclawRoot,
179
268
  } = {}) => {
269
+ const bundledPackageRoot = getBundledOpenclawPackageRoot({
270
+ fsModule,
271
+ resolveImpl,
272
+ });
180
273
  const bundledVersion = readBundledOpenclawVersion({
181
274
  fsModule,
182
275
  resolveImpl,
@@ -194,26 +287,77 @@ const syncManagedOpenclawRuntimeWithBundled = ({
194
287
  fsModule,
195
288
  runtimeDir,
196
289
  });
290
+ const runtimePackageRoot = getManagedOpenclawPackageRoot({ runtimeDir });
291
+ const runtimePackageRootIsSymlink = isPackageRootSymlink({
292
+ fsModule,
293
+ packageRoot: runtimePackageRoot,
294
+ });
295
+ const bundledFingerprint = computePackageFingerprint({
296
+ fsModule,
297
+ packageRoot: bundledPackageRoot,
298
+ });
299
+ const runtimeFingerprint = computePackageFingerprint({
300
+ fsModule,
301
+ packageRoot: runtimePackageRoot,
302
+ packageJsonPath: getManagedOpenclawPackageJsonPath({ runtimeDir }),
303
+ });
197
304
  if (runtimeVersion && compareVersionParts(runtimeVersion, bundledVersion) >= 0) {
198
- return {
199
- checked: true,
200
- synced: false,
201
- bundledVersion,
202
- runtimeVersion,
203
- };
305
+ if (
306
+ compareVersionParts(runtimeVersion, bundledVersion) > 0 ||
307
+ (!runtimePackageRootIsSymlink &&
308
+ (!bundledFingerprint || runtimeFingerprint === bundledFingerprint))
309
+ ) {
310
+ return {
311
+ checked: true,
312
+ synced: false,
313
+ bundledVersion,
314
+ runtimeVersion,
315
+ };
316
+ }
317
+ logger.log(
318
+ runtimePackageRootIsSymlink
319
+ ? `[alphaclaw] Managed OpenClaw runtime ${runtimeVersion} is symlinked to the bundled package; refreshing runtime...`
320
+ : `[alphaclaw] Managed OpenClaw runtime ${runtimeVersion} differs from bundled ${bundledVersion}; refreshing runtime...`,
321
+ );
322
+ } else {
323
+ logger.log(
324
+ runtimeVersion
325
+ ? `[alphaclaw] Managed OpenClaw runtime ${runtimeVersion} is older than bundled ${bundledVersion}; syncing runtime...`
326
+ : `[alphaclaw] Managed OpenClaw runtime missing; seeding bundled OpenClaw ${bundledVersion}...`,
327
+ );
328
+ }
329
+
330
+ if (!runtimeVersion) {
331
+ try {
332
+ const seedResult = seedManagedOpenclawRuntimeFromBundledInstall({
333
+ execSyncImpl,
334
+ fsModule,
335
+ logger,
336
+ runtimeDir,
337
+ bundledPackageRoot,
338
+ alphaclawRoot,
339
+ });
340
+ if (seedResult.seeded) {
341
+ return {
342
+ checked: true,
343
+ synced: true,
344
+ bundledVersion,
345
+ runtimeVersion: seedResult.version || bundledVersion,
346
+ };
347
+ }
348
+ } catch (error) {
349
+ logger.log(
350
+ `[alphaclaw] Could not seed managed OpenClaw runtime from bundled node_modules: ${error.message}`,
351
+ );
352
+ }
204
353
  }
205
354
 
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
355
  const installResult = installManagedOpenclawRuntime({
212
356
  execSyncImpl,
213
357
  fsModule,
214
358
  logger,
215
359
  runtimeDir,
216
- spec: `openclaw@${bundledVersion}`,
360
+ sourcePath: bundledPackageRoot,
217
361
  alphaclawRoot,
218
362
  });
219
363
  return {
@@ -248,13 +392,16 @@ const prependManagedOpenclawBinToPath = ({
248
392
  module.exports = {
249
393
  applyManagedOpenclawPatch,
250
394
  ensureManagedOpenclawRuntimeProject,
395
+ getBundledOpenclawPackageRoot,
251
396
  getManagedOpenclawBinDir,
252
397
  getManagedOpenclawBinPath,
398
+ getManagedOpenclawPackageRoot,
253
399
  getManagedOpenclawPackageJsonPath,
254
400
  getManagedOpenclawRuntimeDir,
255
401
  installManagedOpenclawRuntime,
256
402
  prependManagedOpenclawBinToPath,
257
403
  readBundledOpenclawVersion,
258
404
  readManagedOpenclawRuntimeVersion,
405
+ seedManagedOpenclawRuntimeFromBundledInstall,
259
406
  syncManagedOpenclawRuntimeWithBundled,
260
407
  };
@@ -0,0 +1,274 @@
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 resolveInstallRootFromPackageRoot = ({ packageRoot } = {}) => {
146
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
147
+ if (!resolvedPackageRoot) return "";
148
+ const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
149
+ const nodeModulesIndex = resolvedPackageRoot.lastIndexOf(nodeModulesSegment);
150
+ if (nodeModulesIndex < 0) {
151
+ return resolvedPackageRoot;
152
+ }
153
+ return resolvedPackageRoot.slice(0, nodeModulesIndex);
154
+ };
155
+
156
+ const seedRuntimeFromBundledInstall = ({
157
+ fsModule = fs,
158
+ packageRoot,
159
+ runtimeDir,
160
+ runtimePackageJson,
161
+ } = {}) => {
162
+ const installRoot = resolveInstallRootFromPackageRoot({ packageRoot });
163
+ const bundledNodeModulesPath = path.join(installRoot, "node_modules");
164
+ if (!installRoot || !fsModule.existsSync(bundledNodeModulesPath)) {
165
+ return {
166
+ seeded: false,
167
+ installRoot,
168
+ bundledNodeModulesPath,
169
+ };
170
+ }
171
+
172
+ const resolvedRuntimeDir = path.resolve(String(runtimeDir || ""));
173
+ const runtimeParentDir = path.dirname(resolvedRuntimeDir);
174
+ fsModule.mkdirSync(runtimeParentDir, { recursive: true });
175
+ const tempRuntimeDir = fsModule.mkdtempSync(
176
+ path.join(runtimeParentDir, `${path.basename(resolvedRuntimeDir)}-seed-`),
177
+ );
178
+ let seeded = false;
179
+ try {
180
+ if (runtimePackageJson) {
181
+ fsModule.writeFileSync(
182
+ path.join(tempRuntimeDir, "package.json"),
183
+ JSON.stringify(runtimePackageJson, null, 2),
184
+ );
185
+ }
186
+ fsModule.cpSync(
187
+ bundledNodeModulesPath,
188
+ path.join(tempRuntimeDir, "node_modules"),
189
+ {
190
+ recursive: true,
191
+ dereference: true,
192
+ preserveTimestamps: true,
193
+ },
194
+ );
195
+ try {
196
+ fsModule.rmSync(resolvedRuntimeDir, { recursive: true, force: true });
197
+ } catch {}
198
+ fsModule.renameSync(tempRuntimeDir, resolvedRuntimeDir);
199
+ seeded = true;
200
+ return {
201
+ seeded: true,
202
+ installRoot,
203
+ bundledNodeModulesPath,
204
+ runtimeDir: resolvedRuntimeDir,
205
+ };
206
+ } finally {
207
+ if (!seeded) {
208
+ try {
209
+ fsModule.rmSync(tempRuntimeDir, { recursive: true, force: true });
210
+ } catch {}
211
+ }
212
+ }
213
+ };
214
+
215
+ const packLocalPackageForInstall = ({
216
+ execSyncImpl,
217
+ fsModule = fs,
218
+ packageRoot,
219
+ tempDirPrefix = "alphaclaw-package-pack-",
220
+ } = {}) => {
221
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
222
+ const packDir = fsModule.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
223
+ try {
224
+ const packStdout = String(
225
+ execSyncImpl(
226
+ `npm pack ${shellQuote(resolvedPackageRoot)} --quiet --ignore-scripts --pack-destination ${shellQuote(packDir)}`,
227
+ {
228
+ encoding: "utf8",
229
+ stdio: ["ignore", "pipe", "inherit"],
230
+ timeout: 180000,
231
+ },
232
+ ) || "",
233
+ )
234
+ .trim()
235
+ .split(/\r?\n/)
236
+ .map((entry) => entry.trim())
237
+ .filter(Boolean);
238
+ const packFileName =
239
+ packStdout.at(-1) ||
240
+ fsModule.readdirSync(packDir).find((entry) => entry.endsWith(".tgz"));
241
+ if (!packFileName) {
242
+ throw new Error(`npm pack did not produce a tarball for ${resolvedPackageRoot}`);
243
+ }
244
+ const tarballPath = path.join(packDir, packFileName);
245
+ if (!fsModule.existsSync(tarballPath)) {
246
+ throw new Error(`Packed tarball missing at ${tarballPath}`);
247
+ }
248
+ return {
249
+ tarballPath,
250
+ cleanup: () => {
251
+ try {
252
+ fsModule.rmSync(packDir, { recursive: true, force: true });
253
+ } catch {}
254
+ },
255
+ };
256
+ } catch (error) {
257
+ try {
258
+ fsModule.rmSync(packDir, { recursive: true, force: true });
259
+ } catch {}
260
+ throw error;
261
+ }
262
+ };
263
+
264
+ const shellQuote = (value) =>
265
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
266
+
267
+ module.exports = {
268
+ computePackageFingerprint,
269
+ isPackageRootSymlink,
270
+ packLocalPackageForInstall,
271
+ resolveInstallRootFromPackageRoot,
272
+ resolvePackageRootFromEntryPath,
273
+ seedRuntimeFromBundledInstall,
274
+ };
@@ -1,3 +1,5 @@
1
+ const path = require("path");
2
+
1
3
  const {
2
4
  installManagedAlphaclawRuntime,
3
5
  } = require("./alphaclaw-runtime");
@@ -36,13 +38,23 @@ const applyPendingAlphaclawUpdate = ({
36
38
  const spec = buildPendingAlphaclawInstallSpec(marker);
37
39
  logger.log(`[alphaclaw] Pending update detected, installing ${spec}...`);
38
40
 
41
+ const resolvedInstallDir = path.resolve(String(installDir || ""));
42
+ const installParentDir = path.dirname(resolvedInstallDir);
43
+ const tempInstallDir = fsModule.mkdtempSync(
44
+ path.join(installParentDir, `${path.basename(resolvedInstallDir)}-pending-`),
45
+ );
46
+
39
47
  try {
40
48
  installManagedAlphaclawRuntime({
41
49
  execSyncImpl,
42
50
  fsModule,
43
- runtimeDir: installDir,
51
+ runtimeDir: tempInstallDir,
44
52
  spec,
45
53
  });
54
+ try {
55
+ fsModule.rmSync(resolvedInstallDir, { recursive: true, force: true });
56
+ } catch {}
57
+ fsModule.renameSync(tempInstallDir, resolvedInstallDir);
46
58
  fsModule.unlinkSync(markerPath);
47
59
  logger.log("[alphaclaw] Update applied successfully");
48
60
  return {
@@ -52,6 +64,9 @@ const applyPendingAlphaclawUpdate = ({
52
64
  };
53
65
  } catch (error) {
54
66
  logger.log(`[alphaclaw] Update install failed: ${error.message}`);
67
+ try {
68
+ fsModule.rmSync(tempInstallDir, { recursive: true, force: true });
69
+ } catch {}
55
70
  try {
56
71
  fsModule.unlinkSync(markerPath);
57
72
  } catch {}
@@ -1,3 +1,5 @@
1
+ const path = require("path");
2
+
1
3
  const {
2
4
  installManagedOpenclawRuntime,
3
5
  } = require("./openclaw-runtime");
@@ -36,14 +38,24 @@ const applyPendingOpenclawUpdate = ({
36
38
  const spec = buildPendingOpenclawInstallSpec(marker);
37
39
  logger.log(`[alphaclaw] Pending OpenClaw update detected, installing ${spec}...`);
38
40
 
41
+ const resolvedInstallDir = path.resolve(String(installDir || ""));
42
+ const installParentDir = path.dirname(resolvedInstallDir);
43
+ const tempInstallDir = fsModule.mkdtempSync(
44
+ path.join(installParentDir, `${path.basename(resolvedInstallDir)}-pending-`),
45
+ );
46
+
39
47
  try {
40
48
  installManagedOpenclawRuntime({
41
49
  execSyncImpl,
42
50
  fsModule,
43
51
  logger,
44
- runtimeDir: installDir,
52
+ runtimeDir: tempInstallDir,
45
53
  spec,
46
54
  });
55
+ try {
56
+ fsModule.rmSync(resolvedInstallDir, { recursive: true, force: true });
57
+ } catch {}
58
+ fsModule.renameSync(tempInstallDir, resolvedInstallDir);
47
59
  fsModule.unlinkSync(markerPath);
48
60
  logger.log("[alphaclaw] OpenClaw update applied successfully");
49
61
  return {
@@ -53,6 +65,9 @@ const applyPendingOpenclawUpdate = ({
53
65
  };
54
66
  } catch (error) {
55
67
  logger.log(`[alphaclaw] OpenClaw update install failed: ${error.message}`);
68
+ try {
69
+ fsModule.rmSync(tempInstallDir, { recursive: true, force: true });
70
+ } catch {}
56
71
  try {
57
72
  fsModule.unlinkSync(markerPath);
58
73
  } catch {}
@@ -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.2",
3
+ "version": "0.8.7-beta.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },