@chrysb/alphaclaw 0.8.7-beta.2 → 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
@@ -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,35 @@ 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
+ } = require("./package-fingerprint");
6
11
 
7
12
  const getManagedAlphaclawRuntimeDir = ({ rootDir = kRootDir } = {}) =>
8
13
  path.join(rootDir, ".alphaclaw-runtime");
9
14
 
10
- const getManagedAlphaclawCliPath = ({ runtimeDir } = {}) =>
15
+ const getBundledAlphaclawPackageRoot = () => path.resolve(__dirname, "..", "..");
16
+
17
+ const getManagedAlphaclawPackageRoot = ({ runtimeDir } = {}) =>
11
18
  path.join(
12
19
  runtimeDir || getManagedAlphaclawRuntimeDir(),
13
20
  "node_modules",
14
21
  "@chrysb",
15
22
  "alphaclaw",
23
+ );
24
+
25
+ const getManagedAlphaclawCliPath = ({ runtimeDir } = {}) =>
26
+ path.join(
27
+ getManagedAlphaclawPackageRoot({ runtimeDir }),
16
28
  "bin",
17
29
  "alphaclaw.js",
18
30
  );
19
31
 
20
32
  const getManagedAlphaclawPackageJsonPath = ({ runtimeDir } = {}) =>
21
33
  path.join(
22
- runtimeDir || getManagedAlphaclawRuntimeDir(),
23
- "node_modules",
24
- "@chrysb",
25
- "alphaclaw",
34
+ getManagedAlphaclawPackageRoot({ runtimeDir }),
26
35
  "package.json",
27
36
  );
28
37
 
@@ -89,21 +98,40 @@ const installManagedAlphaclawRuntime = ({
89
98
  fsModule = fs,
90
99
  runtimeDir,
91
100
  spec,
101
+ sourcePath,
92
102
  } = {}) => {
93
- const normalizedSpec =
94
- String(spec || "").trim() || "@chrysb/alphaclaw@latest";
103
+ const normalizedSourcePath = String(sourcePath || "").trim();
104
+ const normalizedSpec = normalizedSourcePath
105
+ ? normalizedSourcePath
106
+ : String(spec || "").trim() || "@chrysb/alphaclaw@latest";
95
107
  ensureManagedAlphaclawRuntimeProject({
96
108
  fsModule,
97
109
  runtimeDir,
98
110
  });
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
- );
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
+ }
107
135
  return {
108
136
  spec: normalizedSpec,
109
137
  version: readManagedAlphaclawRuntimeVersion({
@@ -118,11 +146,12 @@ const syncManagedAlphaclawRuntimeWithBundled = ({
118
146
  fsModule = fs,
119
147
  logger = console,
120
148
  runtimeDir,
149
+ packageRoot = getBundledAlphaclawPackageRoot(),
121
150
  packageJsonPath,
122
151
  } = {}) => {
123
152
  const bundledVersion = readBundledAlphaclawVersion({
124
153
  fsModule,
125
- packageJsonPath,
154
+ packageJsonPath: packageJsonPath || path.join(packageRoot, "package.json"),
126
155
  });
127
156
  if (!bundledVersion) {
128
157
  return {
@@ -140,25 +169,52 @@ const syncManagedAlphaclawRuntimeWithBundled = ({
140
169
  fsModule,
141
170
  runtimeDir,
142
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
+ });
143
187
  if (runtimeVersion && compareVersionParts(runtimeVersion, bundledVersion) >= 0) {
144
- return {
145
- checked: true,
146
- synced: false,
147
- bundledVersion,
148
- runtimeVersion,
149
- };
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
+ );
150
211
  }
151
212
 
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
213
  const installResult = installManagedAlphaclawRuntime({
158
214
  execSyncImpl,
159
215
  fsModule,
160
216
  runtimeDir,
161
- spec: `@chrysb/alphaclaw@${bundledVersion}`,
217
+ sourcePath: packageRoot,
162
218
  });
163
219
  return {
164
220
  checked: true,
@@ -170,8 +226,10 @@ const syncManagedAlphaclawRuntimeWithBundled = ({
170
226
 
171
227
  module.exports = {
172
228
  ensureManagedAlphaclawRuntimeProject,
229
+ getBundledAlphaclawPackageRoot,
173
230
  getManagedAlphaclawCliPath,
174
231
  getManagedAlphaclawPackageJsonPath,
232
+ getManagedAlphaclawPackageRoot,
175
233
  getManagedAlphaclawRuntimeDir,
176
234
  installManagedAlphaclawRuntime,
177
235
  readBundledAlphaclawVersion,
@@ -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
+ };
@@ -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.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },