@deeplake/hivemind 0.7.60 → 0.7.62

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.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/bundle/cli.js +34 -0
  4. package/codex/bundle/capture.js +34 -0
  5. package/codex/bundle/commands/auth-login.js +34 -0
  6. package/codex/bundle/graph-pull-worker.js +34 -0
  7. package/codex/bundle/pre-tool-use.js +34 -0
  8. package/codex/bundle/session-start-setup.js +34 -0
  9. package/codex/bundle/session-start.js +34 -0
  10. package/codex/bundle/shell/deeplake-shell.js +34 -0
  11. package/codex/bundle/stop.js +34 -0
  12. package/cursor/bundle/capture.js +34 -0
  13. package/cursor/bundle/commands/auth-login.js +34 -0
  14. package/cursor/bundle/graph-pull-worker.js +34 -0
  15. package/cursor/bundle/pre-tool-use.js +34 -0
  16. package/cursor/bundle/session-start.js +34 -0
  17. package/cursor/bundle/shell/deeplake-shell.js +34 -0
  18. package/hermes/bundle/capture.js +34 -0
  19. package/hermes/bundle/commands/auth-login.js +34 -0
  20. package/hermes/bundle/graph-pull-worker.js +34 -0
  21. package/hermes/bundle/pre-tool-use.js +34 -0
  22. package/hermes/bundle/session-start.js +34 -0
  23. package/hermes/bundle/shell/deeplake-shell.js +34 -0
  24. package/mcp/bundle/server.js +34 -0
  25. package/openclaw/dist/index.js +29 -1
  26. package/openclaw/openclaw.plugin.json +1 -1
  27. package/openclaw/package.json +1 -1
  28. package/package.json +2 -1
  29. package/scripts/audit-openclaw-bundle.mjs +187 -0
  30. package/scripts/ensure-tree-sitter.mjs +91 -0
  31. package/scripts/pack-check.mjs +29 -0
  32. package/scripts/sync-versions.d.mts +15 -0
  33. package/scripts/sync-versions.mjs +103 -0
  34. package/scripts/verify-install.sh +218 -0
@@ -23683,6 +23683,9 @@ function traceSql(msg) {
23683
23683
  log3(msg);
23684
23684
  }
23685
23685
  var _signalledBalanceExhausted = false;
23686
+ var _signalledLowBalance = false;
23687
+ var LOW_BALANCE_THRESHOLD_CENTS = 200;
23688
+ var BALANCE_HEADER = "X-Activeloop-Balance-Cents";
23686
23689
  function maybeSignalBalanceExhausted(status, bodyText) {
23687
23690
  if (status !== 402)
23688
23691
  return;
@@ -23703,6 +23706,35 @@ function maybeSignalBalanceExhausted(status, bodyText) {
23703
23706
  log3(`enqueue balance-exhausted failed: ${e instanceof Error ? e.message : String(e)}`);
23704
23707
  });
23705
23708
  }
23709
+ function signalLowBalanceFromHeader(resp) {
23710
+ if (_signalledLowBalance)
23711
+ return;
23712
+ const raw = resp.headers?.get?.(BALANCE_HEADER);
23713
+ if (!raw)
23714
+ return;
23715
+ if (!/^-?\d+$/.test(raw.trim()))
23716
+ return;
23717
+ const balance = Number(raw.trim());
23718
+ if (!Number.isFinite(balance))
23719
+ return;
23720
+ if (balance >= LOW_BALANCE_THRESHOLD_CENTS)
23721
+ return;
23722
+ if (balance <= 0)
23723
+ return;
23724
+ _signalledLowBalance = true;
23725
+ log3(`balance below threshold (${balance}\xA2) \u2014 enqueuing low-balance banner`);
23726
+ enqueueNotification({
23727
+ id: "low-balance-warning",
23728
+ severity: "warn",
23729
+ transient: true,
23730
+ title: "Your org's Hivemind balance is running low",
23731
+ body: `Only $${(balance / 100).toFixed(2)} of prepaid balance remains. Admins can top up at ${billingUrl()}; otherwise ask an org admin to top up before requests start failing.`,
23732
+ dedupKey: { reason: "low-balance" }
23733
+ }).catch((e) => {
23734
+ _signalledLowBalance = false;
23735
+ log3(`enqueue low-balance failed: ${e instanceof Error ? e.message : String(e)}`);
23736
+ });
23737
+ }
23706
23738
  function billingUrl() {
23707
23739
  try {
23708
23740
  const c = loadCredentials();
@@ -23828,6 +23860,7 @@ var DeeplakeApi = class {
23828
23860
  }
23829
23861
  throw lastError;
23830
23862
  }
23863
+ signalLowBalanceFromHeader(resp);
23831
23864
  if (resp.ok) {
23832
23865
  const raw = await resp.json();
23833
23866
  if (!raw?.rows || !raw?.columns)
@@ -23988,6 +24021,7 @@ var DeeplakeApi = class {
23988
24021
  ...deeplakeClientHeader()
23989
24022
  }
23990
24023
  });
24024
+ signalLowBalanceFromHeader(resp);
23991
24025
  if (resp.ok) {
23992
24026
  const data = await resp.json();
23993
24027
  return {
@@ -494,6 +494,9 @@ function traceSql(msg) {
494
494
  if (globalThis.__hivemind_tuning__.HIVEMIND_DEBUG === "1") log3(msg);
495
495
  }
496
496
  var _signalledBalanceExhausted = false;
497
+ var _signalledLowBalance = false;
498
+ var LOW_BALANCE_THRESHOLD_CENTS = 200;
499
+ var BALANCE_HEADER = "X-Activeloop-Balance-Cents";
497
500
  function maybeSignalBalanceExhausted(status, bodyText) {
498
501
  if (status !== 402) return;
499
502
  if (!bodyText.includes("balance_cents")) return;
@@ -511,6 +514,29 @@ function maybeSignalBalanceExhausted(status, bodyText) {
511
514
  log3(`enqueue balance-exhausted failed: ${e instanceof Error ? e.message : String(e)}`);
512
515
  });
513
516
  }
517
+ function signalLowBalanceFromHeader(resp) {
518
+ if (_signalledLowBalance) return;
519
+ const raw = resp.headers?.get?.(BALANCE_HEADER);
520
+ if (!raw) return;
521
+ if (!/^-?\d+$/.test(raw.trim())) return;
522
+ const balance = Number(raw.trim());
523
+ if (!Number.isFinite(balance)) return;
524
+ if (balance >= LOW_BALANCE_THRESHOLD_CENTS) return;
525
+ if (balance <= 0) return;
526
+ _signalledLowBalance = true;
527
+ log3(`balance below threshold (${balance}\xA2) \u2014 enqueuing low-balance banner`);
528
+ enqueueNotification({
529
+ id: "low-balance-warning",
530
+ severity: "warn",
531
+ transient: true,
532
+ title: "Your org's Hivemind balance is running low",
533
+ body: `Only $${(balance / 100).toFixed(2)} of prepaid balance remains. Admins can top up at ${billingUrl()}; otherwise ask an org admin to top up before requests start failing.`,
534
+ dedupKey: { reason: "low-balance" }
535
+ }).catch((e) => {
536
+ _signalledLowBalance = false;
537
+ log3(`enqueue low-balance failed: ${e instanceof Error ? e.message : String(e)}`);
538
+ });
539
+ }
514
540
  function billingUrl() {
515
541
  try {
516
542
  const c = loadCredentials();
@@ -636,6 +662,7 @@ var DeeplakeApi = class {
636
662
  }
637
663
  throw lastError;
638
664
  }
665
+ signalLowBalanceFromHeader(resp);
639
666
  if (resp.ok) {
640
667
  const raw = await resp.json();
641
668
  if (!raw?.rows || !raw?.columns) return [];
@@ -797,6 +824,7 @@ var DeeplakeApi = class {
797
824
  ...deeplakeClientHeader()
798
825
  }
799
826
  });
827
+ signalLowBalanceFromHeader(resp);
800
828
  if (resp.ok) {
801
829
  const data = await resp.json();
802
830
  return {
@@ -1795,7 +1823,7 @@ function extractLatestVersion(body) {
1795
1823
  return typeof v === "string" && v.length > 0 ? v : null;
1796
1824
  }
1797
1825
  function getInstalledVersion() {
1798
- return "0.7.60".length > 0 ? "0.7.60" : null;
1826
+ return "0.7.62".length > 0 ? "0.7.62" : null;
1799
1827
  }
1800
1828
  function isNewer(latest, current) {
1801
1829
  const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
@@ -54,5 +54,5 @@
54
54
  }
55
55
  }
56
56
  },
57
- "version": "0.7.60"
57
+ "version": "0.7.62"
58
58
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hivemind",
3
- "version": "0.7.60",
3
+ "version": "0.7.62",
4
4
  "type": "module",
5
5
  "description": "Hivemind — cloud-backed persistent shared memory for AI agents, powered by DeepLake",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeplake/hivemind",
3
- "version": "0.7.60",
3
+ "version": "0.7.62",
4
4
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
5
5
  "type": "module",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "openclaw/openclaw.plugin.json",
27
27
  "openclaw/package.json",
28
28
  ".claude-plugin",
29
+ "scripts",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local replication of ClawHub's static-analysis scanner against the openclaw
4
+ * plugin bundle. Runs the same per-file regex rules ClawHub uses at publish
5
+ * time so we can see flags BEFORE shipping a release instead of after.
6
+ *
7
+ * Rules are replicated (not imported) from openclaw's skill-scanner — that
8
+ * code lives in our research-reference checkout under ~/al-projects/ext/ and
9
+ * is third-party we don't own. Re-sync these rules if upstream changes them.
10
+ *
11
+ * Reference: openclaw repo, src/security/skill-scanner.ts:147-206
12
+ *
13
+ * Usage:
14
+ * node scripts/audit-openclaw-bundle.mjs # scan openclaw/dist
15
+ * node scripts/audit-openclaw-bundle.mjs <path> # scan a specific dir
16
+ *
17
+ * Exits non-zero if any "critical" or "warn" finding is reported.
18
+ */
19
+
20
+ import { readFileSync, statSync } from "node:fs";
21
+ import { readdir } from "node:fs/promises";
22
+ import { join, extname } from "node:path";
23
+
24
+ // Parse args: positional SCAN_DIR + optional --criticals-only flag.
25
+ //
26
+ // --criticals-only exits non-zero only on `critical` findings, treating
27
+ // `warn` as advisory. Used by the CI release gate so we block on
28
+ // definitively-malicious patterns but tolerate fundamental warns like
29
+ // the worker's "readFileSync + fetch in same file" — irreducible without
30
+ // splitting the worker into multiple shipped files. Local dev still
31
+ // runs with the default strict mode (exits on any finding) so we
32
+ // surface every drift before it ships.
33
+ const rawArgs = process.argv.slice(2);
34
+ const STRICT_MODE = !rawArgs.includes("--criticals-only");
35
+ const SCAN_DIR = rawArgs.find(a => !a.startsWith("--")) ?? "openclaw/dist";
36
+ const SCANNABLE_EXT = new Set([".js", ".mjs", ".cjs"]);
37
+ const MAX_FILE_BYTES = 1024 * 1024; // 1MB; matches upstream default
38
+
39
+ // ---- LINE_RULES (per-line; both pattern AND requiresContext must match) ----
40
+ const LINE_RULES = [
41
+ {
42
+ ruleId: "dangerous-exec",
43
+ severity: "critical",
44
+ message: "Shell command execution detected (child_process)",
45
+ pattern: /\b(exec|execSync|spawn|spawnSync|execFile|execFileSync)\s*\(/,
46
+ requiresContext: /child_process/,
47
+ },
48
+ {
49
+ ruleId: "dynamic-code-execution",
50
+ severity: "critical",
51
+ message: "Dynamic code execution detected",
52
+ pattern: /\beval\s*\(|new\s+Function\s*\(/,
53
+ },
54
+ {
55
+ ruleId: "crypto-mining",
56
+ severity: "critical",
57
+ message: "Possible crypto-mining reference detected",
58
+ pattern: /stratum\+tcp|stratum\+ssl|coinhive|cryptonight|xmrig/i,
59
+ },
60
+ {
61
+ ruleId: "suspicious-network",
62
+ severity: "warn",
63
+ message: "WebSocket connection to non-standard port",
64
+ pattern: /new\s+WebSocket\s*\(\s*["']wss?:\/\/[^"']*:(\d+)/,
65
+ portCheck: true,
66
+ },
67
+ ];
68
+
69
+ const STANDARD_PORTS = new Set([80, 443, 8080, 8443, 3000]);
70
+ const NETWORK_SEND = /\bfetch\s*\(|\bpost\s*\(|\.\s*post\s*\(|http\.request\s*\(/i;
71
+
72
+ // ---- SOURCE_RULES (whole file; both pattern AND requiresContext must match) ----
73
+ const SOURCE_RULES = [
74
+ {
75
+ ruleId: "potential-exfiltration",
76
+ severity: "warn",
77
+ message: "File read combined with network send (possible exfiltration)",
78
+ pattern: /readFileSync|readFile/,
79
+ requiresContext: NETWORK_SEND,
80
+ },
81
+ {
82
+ ruleId: "obfuscated-code-hex",
83
+ severity: "warn",
84
+ message: "Hex-encoded string sequence detected (possible obfuscation)",
85
+ pattern: /(\\x[0-9a-fA-F]{2}){6,}/,
86
+ },
87
+ {
88
+ ruleId: "obfuscated-code-base64",
89
+ severity: "warn",
90
+ message: "Large base64 payload with decode call detected (possible obfuscation)",
91
+ pattern: /(?:atob|Buffer\.from)\s*\(\s*["'][A-Za-z0-9+/=]{200,}["']/,
92
+ },
93
+ {
94
+ ruleId: "env-harvesting",
95
+ severity: "critical",
96
+ message: "Environment variable access combined with network send (possible credential harvesting)",
97
+ pattern: /process\.env/,
98
+ requiresContext: NETWORK_SEND,
99
+ },
100
+ ];
101
+
102
+ function truncate(s, n = 120) { return s.length <= n ? s : s.slice(0, n) + "…"; }
103
+
104
+ function scanFile(path) {
105
+ const stat = statSync(path);
106
+ if (stat.size > MAX_FILE_BYTES) {
107
+ return [{ ruleId: "file-too-large", severity: "info", file: path, line: 0, message: `Skipped (${stat.size} bytes > ${MAX_FILE_BYTES} byte limit)`, evidence: "" }];
108
+ }
109
+ const source = readFileSync(path, "utf-8");
110
+ const lines = source.split("\n");
111
+ const findings = [];
112
+
113
+ for (const rule of LINE_RULES) {
114
+ if (rule.requiresContext && !rule.requiresContext.test(source)) continue;
115
+ for (let i = 0; i < lines.length; i++) {
116
+ const line = lines[i];
117
+ const m = rule.pattern.exec(line);
118
+ if (!m) continue;
119
+ if (rule.portCheck) {
120
+ const port = Number.parseInt(m[1], 10);
121
+ if (STANDARD_PORTS.has(port)) continue;
122
+ }
123
+ findings.push({ ruleId: rule.ruleId, severity: rule.severity, file: path, line: i + 1, message: rule.message, evidence: truncate(line.trim()) });
124
+ break; // one finding per line-rule per file
125
+ }
126
+ }
127
+
128
+ for (const rule of SOURCE_RULES) {
129
+ if (!rule.pattern.test(source)) continue;
130
+ if (rule.requiresContext && !rule.requiresContext.test(source)) continue;
131
+ let matchLine = 0, matchEvidence = "";
132
+ for (let i = 0; i < lines.length; i++) {
133
+ if (rule.pattern.test(lines[i])) { matchLine = i + 1; matchEvidence = lines[i].trim(); break; }
134
+ }
135
+ if (matchLine === 0) { matchLine = 1; matchEvidence = source.slice(0, 120); }
136
+ findings.push({ ruleId: rule.ruleId, severity: rule.severity, file: path, line: matchLine, message: rule.message, evidence: truncate(matchEvidence) });
137
+ }
138
+
139
+ return findings;
140
+ }
141
+
142
+ async function* walk(dir) {
143
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
144
+ const p = join(dir, entry.name);
145
+ if (entry.isDirectory()) yield* walk(p);
146
+ else if (entry.isFile() && SCANNABLE_EXT.has(extname(entry.name).toLowerCase())) yield p;
147
+ }
148
+ }
149
+
150
+ const SEVERITY_RANK = { info: 0, warn: 1, critical: 2 };
151
+ const SEVERITY_ICON = { info: "·", warn: "!", critical: "✗" };
152
+
153
+ const allFindings = [];
154
+ let scannedFiles = 0;
155
+ for await (const file of walk(SCAN_DIR)) {
156
+ scannedFiles++;
157
+ for (const f of scanFile(file)) allFindings.push(f);
158
+ }
159
+
160
+ const counts = { info: 0, warn: 0, critical: 0 };
161
+ for (const f of allFindings) counts[f.severity] = (counts[f.severity] ?? 0) + 1;
162
+
163
+ console.log(`\nScanned ${scannedFiles} file(s) under ${SCAN_DIR}/\n`);
164
+
165
+ if (allFindings.length === 0) {
166
+ console.log("✓ No findings. Bundle is clean against ClawHub's static-analysis rules.\n");
167
+ process.exit(0);
168
+ }
169
+
170
+ allFindings.sort((a, b) => (SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]) || a.file.localeCompare(b.file) || a.line - b.line);
171
+ for (const f of allFindings) {
172
+ console.log(`${SEVERITY_ICON[f.severity]} [${f.severity.toUpperCase()}] ${f.ruleId}`);
173
+ console.log(` ${f.file}:${f.line}`);
174
+ console.log(` ${f.message}`);
175
+ if (f.evidence) console.log(` > ${f.evidence}`);
176
+ console.log();
177
+ }
178
+
179
+ const summary = `${counts.critical} critical, ${counts.warn} warn, ${counts.info} info`;
180
+ console.log(`Summary: ${summary}\n`);
181
+ if (!STRICT_MODE && counts.critical === 0 && counts.warn > 0) {
182
+ console.log(`(--criticals-only: warns are advisory and do NOT block. Re-run without the flag for strict local checks.)\n`);
183
+ }
184
+ const shouldFail = STRICT_MODE
185
+ ? (counts.critical > 0 || counts.warn > 0)
186
+ : counts.critical > 0;
187
+ process.exit(shouldFail ? 1 : 0);
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // Ensures the native tree-sitter bindings are loadable on this platform / Node ABI.
3
+ //
4
+ // Why this exists: tree-sitter@0.21.x ships no linux-arm64 prebuild, and
5
+ // tree-sitter-typescript@0.23.x ships a mislabeled (x86-64) one. On linux-arm64
6
+ // both must be compiled from source, and under Node >=22 that compile requires C++20
7
+ // (tree-sitter@0.21's binding.gyp does not request it). tree-sitter is declared as an
8
+ // optionalDependency so this expected arm64 build failure does not abort `npm install`;
9
+ // this script then heals it afterwards.
10
+ //
11
+ // On platforms where the shipped prebuilds work (x64 / darwin / CI) this is a fast
12
+ // no-op and never touches anything. It is intentionally non-fatal: if no toolchain is
13
+ // available it warns and exits 0 rather than breaking the install.
14
+ import { execSync } from 'node:child_process';
15
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
16
+ import { createRequire } from 'node:module';
17
+
18
+ const ROOT = process.cwd();
19
+ const require = createRequire(`${ROOT}/`);
20
+ const PKGS = ['tree-sitter', 'tree-sitter-typescript'];
21
+
22
+ function bindingsLoad() {
23
+ try {
24
+ const Parser = require('tree-sitter');
25
+ const TS = require('tree-sitter-typescript').typescript;
26
+ const parser = new Parser();
27
+ parser.setLanguage(TS);
28
+ parser.parse('const x = 1;');
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ if (process.env.ENSURE_TS_RUNNING) process.exit(0); // recursion guard for the nested npm calls below
36
+ if (bindingsLoad()) process.exit(0); // healthy prebuild / prior build → nothing to do
37
+
38
+ console.error('[ensure-tree-sitter] native bindings not loadable on this platform — building from source...');
39
+
40
+ const pkg = JSON.parse(readFileSync(`${ROOT}/package.json`, 'utf8'));
41
+ const declared = { ...pkg.dependencies, ...pkg.optionalDependencies };
42
+
43
+ const env = { ...process.env, ENSURE_TS_RUNNING: '1' };
44
+ if (process.platform !== 'win32') {
45
+ // Node >=22 V8 headers require C++20; tree-sitter@0.21's binding.gyp doesn't request it.
46
+ env.CXXFLAGS = `${process.env.CXXFLAGS ?? ''} -std=c++20`.trim();
47
+ }
48
+ const run = (cmd) => execSync(cmd, { stdio: 'inherit', env, cwd: ROOT });
49
+
50
+ try {
51
+ // 1. Re-fetch any package npm dropped — an optional dependency whose build failed is
52
+ // removed from node_modules. --ignore-scripts: fetch only, so the compile below is
53
+ // the single source of truth and the project build isn't triggered prematurely.
54
+ const missing = PKGS.filter((n) => !existsSync(`${ROOT}/node_modules/${n}/package.json`));
55
+ if (missing.length) {
56
+ const specs = missing.map((n) => `${n}@${declared[n] ?? 'latest'}`);
57
+ run(`npm install ${specs.join(' ')} --no-save --ignore-scripts`);
58
+ }
59
+
60
+ // 2. Force a from-source compile. These packages install via node-gyp-build, which uses
61
+ // a local prebuild when present and otherwise compiles from source — no network. By
62
+ // removing the (absent or wrong-arch) prebuilds plus any stale build, the rebuild is
63
+ // guaranteed to compile locally, and node-gyp-build loads build/Release ahead of
64
+ // prebuilds, so the correct binary always wins.
65
+ for (const n of PKGS) {
66
+ rmSync(`${ROOT}/node_modules/${n}/prebuilds`, { recursive: true, force: true });
67
+ rmSync(`${ROOT}/node_modules/${n}/build`, { recursive: true, force: true });
68
+ }
69
+ run(`npm rebuild ${PKGS.join(' ')}`);
70
+ } catch (err) {
71
+ console.error('[ensure-tree-sitter] rebuild command failed:', err.message);
72
+ }
73
+
74
+ if (bindingsLoad()) {
75
+ console.error('[ensure-tree-sitter] OK — bindings compiled from source and loadable.');
76
+ process.exit(0);
77
+ }
78
+
79
+ // Strict mode: turn the warning into a hard failure. Opt-in via
80
+ // HIVEMIND_STRICT_POSTINSTALL=1 — set by this repo's own CI workflows so a
81
+ // heal failure surfaces as a red check on the PR instead of getting swallowed
82
+ // and re-emerging downstream as `tsc: Cannot find module 'tree-sitter'`.
83
+ // Default stays non-fatal so end-user consumers of @deeplake/hivemind never
84
+ // get a hard install break — the runtime check at use-time is enough for them.
85
+ const strict = process.env.HIVEMIND_STRICT_POSTINSTALL === '1';
86
+ console.error(
87
+ '[ensure-tree-sitter] WARNING: tree-sitter bindings still unavailable. ' +
88
+ 'Install a C/C++ toolchain and re-run `npm run rebuild:native`.' +
89
+ (strict ? ' (strict mode — failing this install)' : ' (non-fatal)'),
90
+ );
91
+ process.exit(strict ? 1 : 0);
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // Refuse a publish if `npm pack` would include filenames that should
3
+ // never ship to npm — credentials, CI workflows, git internals.
4
+ // Catches a future PR widening package.json's `files` array (or
5
+ // switching to a permissive .npmignore) before any token is touched.
6
+
7
+ import { execFileSync } from 'node:child_process';
8
+
9
+ const FORBIDDEN = [
10
+ /(^|\/)\.npmrc$/,
11
+ /(^|\/)\.env($|\.)/,
12
+ /(^|\/)secrets?(\/|$)/,
13
+ /(^|\/)\.github(\/|$)/,
14
+ /(^|\/)\.git(\/|$)/,
15
+ ];
16
+
17
+ const raw = execFileSync('npm', ['pack', '--dry-run', '--json'], {
18
+ encoding: 'utf8',
19
+ stdio: ['ignore', 'pipe', 'inherit'],
20
+ });
21
+ const entries = JSON.parse(raw)[0].files.map((f) => f.path);
22
+ const hits = entries.filter((p) => FORBIDDEN.some((rx) => rx.test(p)));
23
+
24
+ if (hits.length) {
25
+ console.error('Refusing to publish — forbidden filenames in tarball:');
26
+ for (const h of hits) console.error(' ' + h);
27
+ process.exit(1);
28
+ }
29
+ console.log(`pack-check OK — ${entries.length} files, no forbidden patterns`);
@@ -0,0 +1,15 @@
1
+ export declare const SCALAR_TARGETS: readonly string[];
2
+ export declare const MARKETPLACE_PATH: string;
3
+
4
+ export interface SyncResult {
5
+ writes: number;
6
+ skips: number;
7
+ version: string;
8
+ }
9
+
10
+ export interface SyncOptions {
11
+ root?: string;
12
+ log?: (msg: string) => void;
13
+ }
14
+
15
+ export declare function syncVersions(opts?: SyncOptions): SyncResult;
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ // Sync the version field across all manifests from the single source-of-truth
3
+ // in package.json. Runs as a `prebuild` hook so esbuild's version-define
4
+ // inlines the same value into bundles.
5
+ //
6
+ // Idempotent: skips writes when a target already matches.
7
+ // Exits non-zero if any target file is missing or if package.json has no version.
8
+
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const SOURCE = "package.json";
14
+
15
+ // Scalar targets: each has a single top-level `version` field tracking package.json.
16
+ export const SCALAR_TARGETS = [
17
+ ".claude-plugin/plugin.json",
18
+ "claude-code/.claude-plugin/plugin.json",
19
+ "openclaw/openclaw.plugin.json",
20
+ "openclaw/package.json",
21
+ "codex/package.json",
22
+ ];
23
+
24
+ // Marketplace target: has BOTH metadata.version AND every plugins[].version.
25
+ export const MARKETPLACE_PATH = ".claude-plugin/marketplace.json";
26
+
27
+ function readJsonAt(root, relPath) {
28
+ const full = resolve(root, relPath);
29
+ if (!existsSync(full)) {
30
+ throw new Error(`sync-versions: missing target ${relPath}`);
31
+ }
32
+ return JSON.parse(readFileSync(full, "utf-8"));
33
+ }
34
+
35
+ function writeJsonAt(root, relPath, obj) {
36
+ writeFileSync(resolve(root, relPath), JSON.stringify(obj, null, 2) + "\n");
37
+ }
38
+
39
+ export function syncVersions({ root = process.cwd(), log = (m) => console.error(m) } = {}) {
40
+ const source = readJsonAt(root, SOURCE);
41
+ const version = source.version;
42
+ if (!version || typeof version !== "string") {
43
+ throw new Error(`sync-versions: ${SOURCE} has no string \`version\` field`);
44
+ }
45
+
46
+ let writes = 0;
47
+ let skips = 0;
48
+
49
+ for (const target of SCALAR_TARGETS) {
50
+ const data = readJsonAt(root, target);
51
+ if (data.version === version) {
52
+ log(`sync-versions: ${target} already at ${version}`);
53
+ skips++;
54
+ continue;
55
+ }
56
+ const old = data.version;
57
+ data.version = version;
58
+ writeJsonAt(root, target, data);
59
+ log(`sync-versions: ${target}: ${old} -> ${version}`);
60
+ writes++;
61
+ }
62
+
63
+ const marketplace = readJsonAt(root, MARKETPLACE_PATH);
64
+ let mpChanged = false;
65
+ if (marketplace.metadata?.version !== version) {
66
+ const old = marketplace.metadata?.version;
67
+ marketplace.metadata = marketplace.metadata || {};
68
+ marketplace.metadata.version = version;
69
+ log(`sync-versions: ${MARKETPLACE_PATH} metadata.version: ${old} -> ${version}`);
70
+ mpChanged = true;
71
+ }
72
+ if (Array.isArray(marketplace.plugins)) {
73
+ for (const plugin of marketplace.plugins) {
74
+ if (plugin.version !== version) {
75
+ const old = plugin.version;
76
+ plugin.version = version;
77
+ log(`sync-versions: ${MARKETPLACE_PATH} plugins[${plugin.name}].version: ${old} -> ${version}`);
78
+ mpChanged = true;
79
+ }
80
+ }
81
+ }
82
+ if (mpChanged) {
83
+ writeJsonAt(root, MARKETPLACE_PATH, marketplace);
84
+ writes++;
85
+ } else {
86
+ log(`sync-versions: ${MARKETPLACE_PATH} already at ${version}`);
87
+ skips++;
88
+ }
89
+
90
+ log(`sync-versions: ${writes} written, ${skips} unchanged`);
91
+ return { writes, skips, version };
92
+ }
93
+
94
+ // Script mode — only runs when invoked directly, not when imported by tests.
95
+ const __entryUrl = process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false;
96
+ if (__entryUrl) {
97
+ try {
98
+ syncVersions();
99
+ } catch (e) {
100
+ console.error(e.message);
101
+ process.exit(1);
102
+ }
103
+ }