5533412984
* fix(mcp): drop ${_R%/} parameter-expansion trim that trips Claude Code MCP validator
The POSIX substring trim ${_R%/} is misread by Claude Code's MCP-config
validator as a required env var named "_R%/", causing /doctor to flag
mcp-search as invalid on every install. POSIX collapses // in paths, so
the trim was cosmetic — drop it and the validator passes.
Fixes #2350, #2354, #2356.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(env): block ANTHROPIC_BASE_URL leak + three-branch OAuth-skip predicate
Issue #2375: parent-shell ANTHROPIC_BASE_URL leaked through to subprocess
isolatedEnv, while ANTHROPIC_AUTH_TOKEN was blocked. The OAuth-skip
predicate fired on bare BASE_URL, but no auth credential reached the
subprocess -> "Not logged in". Add ANTHROPIC_BASE_URL to BLOCKED_ENV_VARS
so it can only enter isolatedEnv via ~/.claude-mem/.env.
Replace the OAuth-skip predicate with three branches to prevent a
second-order security regression: a user with a tokenless gateway
configured in .env (BASE_URL only, no token) would otherwise have their
Anthropic OAuth token fetched and sent to their gateway. Token leak to
third party. Three-branch predicate:
1. BASE_URL set -> return without OAuth (custom gateway, never leak token)
2. API_KEY or AUTH_TOKEN set -> return without OAuth (explicit credentials)
3. Otherwise -> OAuth lookup for api.anthropic.com
Adds tests/env-isolation.test.ts.
Fixes #2375.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(worker): classify Claude SDK HTTP 400 as unrecoverable
ClaudeProvider previously had no explicit HTTP 400 handling — the
default branch classified all errors as `transient`, so a permanent
400 (e.g., model rejecting an `effort` parameter forwarded from a
leaked CLAUDE_CODE_EFFORT_LEVEL) would be retried indefinitely
(#1874+ retries observed in one session per #2357).
Mirror GeminiProvider/OpenRouterProvider's pattern: classify 400 as
`unrecoverable`, 401/403 as `auth_invalid`, 429 as `rate_limit`,
default to `transient`. When the 400 body matches the
"effort parameter" signature, emit a one-time SDK warn log pointing
at the env-leak fix in ~/.claude-mem/.env.
Adds tests/claude-provider-error-classifier.test.ts.
Fixes #2357.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chroma): pin onnxruntime>=1.20 + protobuf<7 to fix INVALID_PROTOBUF on macOS arm64
The shipped all-MiniLM-L6-v2 model has pytorch-2.0 IR. chroma-mcp 0.2.6
transitively depends on `chromadb>=1.0.16` which only requires
`onnxruntime>=1.14.1` — uv can therefore resolve to an onnxruntime old
enough to fail every embedding add with `[ONNXRuntimeError] : 7 :
INVALID_PROTOBUF` on macOS arm64 / Python 3.13. Semantic search silently
degraded to FTS-only and smart backfill broke (#2371).
Path B (override) was required because chroma-mcp 0.2.6 is the latest
PyPI release — no upstream bump exists.
Inject `--with onnxruntime>=1.20 --with protobuf<7` into the uvx spawn
args (both persistent and remote modes). The protobuf cap is essential:
forcing only `onnxruntime>=1.20` causes uv to re-resolve and land on
protobuf 7.x, which trips opentelemetry's `_pb2` stubs with `TypeError:
Descriptors cannot be created directly` because they were generated
with protoc <3.19. Capping below 7 lands on protobuf 6.x which
opentelemetry tolerates.
Verified end-to-end: ONNX model loads, embeddings produce a 384-dim
vector, PersistentClient init / add / query roundtrip succeeds:
uvx --python 3.13 --with "onnxruntime>=1.20" --with "protobuf<7" \
chroma-mcp==0.2.6 --help # clean
# programmatic test: onnxruntime 1.26.0, protobuf 6.33.6,
# embedding ok 384, query ok ids=[['1']]
Fixes #2371.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chroma): enforce single chroma-mcp subprocess per worker (#2313)
Root cause: every reconnect path in ChromaMcpManager — connectInternal's
re-entry, the connect-timeout catch, callTool's transport-error retry, and
the transport.onclose handler — used to abandon `this.transport`/`this.client`
by calling at most `transport.close()` and nulling the handles. The MCP SDK's
StdioClientTransport.close() only signals the direct child (uvx); on Linux the
grandchildren (uv -> python -> chroma-mcp) re-parent to init and survive
because the SDK does not put the subprocess in its own process group. Each
reconnect therefore leaked a full chroma-mcp tree, accumulating 20+ instances
per session.
Fix: introduce a private disposeCurrentSubprocess() helper that always tree-
kills via the existing killProcessTree primitive before nulling the transport
reference, and route every "abandon current transport" path (reconnect,
connect-timeout, transport error, onclose, stop) through it. The existing
`connecting: Promise<void> | null` lock continues to serialize concurrent
ensureConnected() callers into a single spawn.
Adds tests/services/sync/chroma-mcp-manager-singleton.test.ts covering:
- 5 parallel ensureConnected() calls produce exactly one spawn
- a transport-error reconnect tree-kills the prior subprocess pid before
spawning a replacement
- stop() disposes state including any pending connecting promise
Manual verification needed on Linux: after a long session with multiple
tool uses, `ps aux | grep chroma-mcp | wc -l` should return 1, not 20+.
Fixes #2313.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): polyfill import.meta.url to __filename in CJS worker bundle
The worker bundles ESM dependencies (notably @anthropic-ai/claude-agent-sdk's
*.mjs files) into CJS output. Those modules call createRequire(import.meta.url)
at module-load time. esbuild's CJS output left this as createRequire(ute.url)
— where `ute` is its `import.meta` polyfill `{}` — so `ute.url` was undefined
and module-load crashed with:
TypeError: The argument 'filename' must be a file URL object, file URL
string, or absolute path string. Received undefined
code: ERR_INVALID_ARG_VALUE
Every Stop hook and every worker subprocess invocation hit this. Fix is the
esbuild `define` option mapping `import.meta.url` to `__filename` (provided as
a real absolute path by the existing CJS prelude in the banner).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: daily dep bump per CLAUDE.md maintenance policy
Root: @anthropic-ai/claude-agent-sdk, @clack/prompts, @types/node,
dompurify, postcss, react, react-dom, yaml, zod.
plugin/: tree-sitter-cli, zod.
openclaw/: @types/node.
All patch/minor bumps; no major version changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build: regenerate plugin artifacts after env/chroma/mcp fixes
Built artifacts are committed so the marketplace-installable plugin
ships with the runtime bundles. Picks up:
- d7b145e9 .mcp.json shell-prelude trim drop
- a8cbd651 EnvManager BASE_URL block + 3-branch predicate
- 8cb73b8c ClaudeProvider HTTP 400 unrecoverable classifier
- ecd5b802 ChromaMcpManager onnxruntime/protobuf overrides
- c79324ea ChromaMcpManager singleton enforcement
- e8376f46 esbuild import.meta.url -> __filename polyfill
- a7541d71 daily dep bump
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build: regenerate plugin artifacts after main merge
Bundles now include both v13.0.0 server-beta runtime (server-beta-service.cjs
+ updated mcp-server.cjs / worker-service.cjs) and this branch's chroma /
env / build / Claude SDK fixes.
Verified: bun test tests/env-isolation.test.ts \\
tests/claude-provider-error-classifier.test.ts \\
tests/services/sync/chroma-mcp-manager-singleton.test.ts
→ 13/13 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(review): address CodeRabbit findings on PR #2394
1. scripts/build-hooks.js — `import.meta.url` now maps to a file:// URL
(via pathToFileURL(__filename).href in the CJS banner) instead of the
raw __filename path. Preserves URL semantics for any bundled ESM dep
that does `new URL(rel, import.meta.url)`. createRequire still works.
2. src/shared/EnvManager.ts — added envFilePath() that resolves
CLAUDE_MEM_ENV_FILE lazily (falling back to paths.envFile()), and
switched internal load/save call sites to use it. ENV_FILE_PATH is
kept as a deprecated snapshot for back-compat. Lets tests target a
temp file without depending on module-load order.
3. tests/env-isolation.test.ts — redirects to a temp dir via
CLAUDE_MEM_ENV_FILE in beforeAll, removes all mutation of the real
~/.claude-mem/.env, and wraps the OAuth-spy assertion in try/finally
so the spy is always restored even if the test fails.
Verified:
bun test tests/env-isolation.test.ts \
tests/claude-provider-error-classifier.test.ts \
tests/services/sync/chroma-mcp-manager-singleton.test.ts
→ 13/13 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
478 lines
18 KiB
JavaScript
478 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { build } from 'esbuild';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
const WORKER_SERVICE = {
|
|
name: 'worker-service',
|
|
source: 'src/services/worker-service.ts'
|
|
};
|
|
|
|
const SERVER_BETA_SERVICE = {
|
|
name: 'server-beta-service',
|
|
source: 'src/server/runtime/ServerBetaService.ts'
|
|
};
|
|
|
|
const MCP_SERVER = {
|
|
name: 'mcp-server',
|
|
source: 'src/servers/mcp-server.ts'
|
|
};
|
|
|
|
const CONTEXT_GENERATOR = {
|
|
name: 'context-generator',
|
|
source: 'src/services/context-generator.ts'
|
|
};
|
|
|
|
function stripHardcodedDirname(filePath) {
|
|
let content = fs.readFileSync(filePath, 'utf-8');
|
|
const before = content.length;
|
|
|
|
const str = `(?:"[^"]*"|'[^']*')`;
|
|
|
|
for (const id of ['__dirname', '__filename']) {
|
|
content = content.replace(new RegExp(`\\bvar ${id}\\s*=\\s*${str},\\s*`, 'g'), 'var ');
|
|
content = content.replace(new RegExp(`\\bvar ${id}\\s*=\\s*${str};\\s*`, 'g'), '');
|
|
content = content.replace(new RegExp(`,\\s*${id}\\s*=\\s*${str}`, 'g'), '');
|
|
}
|
|
|
|
content = content.replace(/\bvar\s*;/g, '');
|
|
content = content.replace(/[ \t]+$/gm, '');
|
|
|
|
const removed = before - content.length;
|
|
if (removed > 0) {
|
|
fs.writeFileSync(filePath, content);
|
|
console.log(` ✓ Stripped hardcoded __dirname/__filename paths (${removed} bytes)`);
|
|
}
|
|
}
|
|
|
|
async function buildHooks() {
|
|
console.log('🔨 Building claude-mem hooks and worker service...\n');
|
|
|
|
try {
|
|
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
const version = packageJson.version;
|
|
console.log(`📌 Version: ${version}`);
|
|
|
|
console.log('\n📦 Preparing output directories...');
|
|
const hooksDir = 'plugin/scripts';
|
|
const uiDir = 'plugin/ui';
|
|
|
|
if (!fs.existsSync(hooksDir)) {
|
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
}
|
|
if (!fs.existsSync(uiDir)) {
|
|
fs.mkdirSync(uiDir, { recursive: true });
|
|
}
|
|
console.log('✓ Output directories ready');
|
|
|
|
console.log('\n📦 Generating plugin package.json...');
|
|
const pluginPackageJson = {
|
|
name: 'claude-mem-plugin',
|
|
version: version,
|
|
private: true,
|
|
description: 'Runtime dependencies for claude-mem bundled hooks',
|
|
type: 'module',
|
|
dependencies: {
|
|
'zod': '^4.3.6',
|
|
'tree-sitter-cli': '^0.26.5',
|
|
'tree-sitter-c': '^0.24.1',
|
|
'tree-sitter-cpp': '^0.23.4',
|
|
'tree-sitter-go': '^0.25.0',
|
|
'tree-sitter-java': '^0.23.5',
|
|
'tree-sitter-javascript': '^0.25.0',
|
|
'tree-sitter-python': '^0.25.0',
|
|
'tree-sitter-ruby': '^0.23.1',
|
|
'tree-sitter-rust': '^0.24.0',
|
|
'tree-sitter-typescript': '^0.23.2',
|
|
'tree-sitter-kotlin': '^0.3.8',
|
|
'tree-sitter-swift': '^0.7.1',
|
|
'tree-sitter-php': '^0.24.2',
|
|
'tree-sitter-elixir': '^0.3.5',
|
|
'@tree-sitter-grammars/tree-sitter-lua': '^0.4.1',
|
|
'tree-sitter-scala': '^0.24.0',
|
|
'tree-sitter-bash': '^0.25.1',
|
|
'tree-sitter-haskell': '^0.23.1',
|
|
'@tree-sitter-grammars/tree-sitter-zig': '^1.1.2',
|
|
'tree-sitter-css': '^0.25.0',
|
|
'tree-sitter-scss': '^1.0.0',
|
|
'@tree-sitter-grammars/tree-sitter-toml': '^0.7.0',
|
|
'@tree-sitter-grammars/tree-sitter-yaml': '^0.7.1',
|
|
'@derekstride/tree-sitter-sql': '^0.3.11',
|
|
'@tree-sitter-grammars/tree-sitter-markdown': '^0.3.2',
|
|
'shell-quote': '^1.8.3',
|
|
},
|
|
overrides: {
|
|
'tree-sitter': '^0.25.0'
|
|
},
|
|
trustedDependencies: [
|
|
'tree-sitter-cli'
|
|
],
|
|
engines: {
|
|
node: '>=18.0.0',
|
|
bun: '>=1.0.0'
|
|
}
|
|
};
|
|
fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n');
|
|
console.log('✓ plugin/package.json generated');
|
|
|
|
console.log('\n📋 Building React viewer...');
|
|
const { spawn } = await import('child_process');
|
|
const viewerBuild = spawn('node', ['scripts/build-viewer.js'], { stdio: 'inherit' });
|
|
await new Promise((resolve, reject) => {
|
|
viewerBuild.on('exit', (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Viewer build failed with exit code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
console.log(`\n🔧 Building worker service...`);
|
|
await build({
|
|
entryPoints: [WORKER_SERVICE.source],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node18',
|
|
format: 'cjs',
|
|
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
|
|
minify: true,
|
|
logLevel: 'error', // Suppress warnings (import.meta warning is benign)
|
|
external: [
|
|
'bun:sqlite',
|
|
'zod',
|
|
'cohere-ai',
|
|
'ollama',
|
|
'@chroma-core/default-embed',
|
|
'onnxruntime-node'
|
|
],
|
|
define: {
|
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`,
|
|
// Polyfill import.meta.url for ESM deps bundled into CJS output.
|
|
// @anthropic-ai/claude-agent-sdk's *.mjs files use createRequire(import.meta.url)
|
|
// and `new URL(rel, import.meta.url)`. We map import.meta.url to a file:// URL
|
|
// (not the raw __filename path) so URL construction preserves its semantics.
|
|
'import.meta.url': '__IMPORT_META_URL__'
|
|
},
|
|
banner: {
|
|
js: [
|
|
'#!/usr/bin/env bun',
|
|
'var __filename = __filename || require("node:path").resolve(process.argv[1] || "");',
|
|
'var __dirname = __dirname || require("node:path").dirname(__filename);',
|
|
'var __IMPORT_META_URL__ = require("node:url").pathToFileURL(__filename).href;'
|
|
].join('\n')
|
|
}
|
|
});
|
|
|
|
stripHardcodedDirname(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
|
|
|
|
fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
|
|
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
|
|
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
|
|
|
|
console.log(`\n🔧 Building server beta service...`);
|
|
await build({
|
|
entryPoints: [SERVER_BETA_SERVICE.source],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node18',
|
|
format: 'cjs',
|
|
outfile: `${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`,
|
|
minify: true,
|
|
logLevel: 'error',
|
|
external: [
|
|
'bun:sqlite',
|
|
'zod',
|
|
],
|
|
define: {
|
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
|
},
|
|
banner: {
|
|
js: [
|
|
'#!/usr/bin/env bun',
|
|
'var __filename = __filename || require("node:path").resolve(process.argv[1] || "");',
|
|
'var __dirname = __dirname || require("node:path").dirname(__filename);'
|
|
].join('\n')
|
|
}
|
|
});
|
|
|
|
stripHardcodedDirname(`${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`);
|
|
|
|
fs.chmodSync(`${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`, 0o755);
|
|
const serverBetaStats = fs.statSync(`${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`);
|
|
console.log(`✓ server-beta-service built (${(serverBetaStats.size / 1024).toFixed(2)} KB)`);
|
|
|
|
console.log(`\n🔧 Building MCP server...`);
|
|
await build({
|
|
entryPoints: [MCP_SERVER.source],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node18',
|
|
format: 'cjs',
|
|
outfile: `${hooksDir}/${MCP_SERVER.name}.cjs`,
|
|
minify: true,
|
|
logLevel: 'error',
|
|
external: [
|
|
'bun:sqlite',
|
|
'tree-sitter-cli',
|
|
'tree-sitter-javascript',
|
|
'tree-sitter-typescript',
|
|
'tree-sitter-python',
|
|
'tree-sitter-go',
|
|
'tree-sitter-rust',
|
|
'tree-sitter-ruby',
|
|
'tree-sitter-java',
|
|
'tree-sitter-c',
|
|
'tree-sitter-cpp',
|
|
'tree-sitter-kotlin',
|
|
'tree-sitter-swift',
|
|
'tree-sitter-php',
|
|
'tree-sitter-elixir',
|
|
'@tree-sitter-grammars/tree-sitter-lua',
|
|
'tree-sitter-scala',
|
|
'tree-sitter-bash',
|
|
'tree-sitter-haskell',
|
|
'@tree-sitter-grammars/tree-sitter-zig',
|
|
'tree-sitter-css',
|
|
'tree-sitter-scss',
|
|
'@tree-sitter-grammars/tree-sitter-toml',
|
|
'@tree-sitter-grammars/tree-sitter-yaml',
|
|
'@derekstride/tree-sitter-sql',
|
|
'@tree-sitter-grammars/tree-sitter-markdown',
|
|
],
|
|
define: {
|
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
|
},
|
|
banner: {
|
|
js: '#!/usr/bin/env node'
|
|
}
|
|
});
|
|
|
|
stripHardcodedDirname(`${hooksDir}/${MCP_SERVER.name}.cjs`);
|
|
|
|
fs.chmodSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 0o755);
|
|
const mcpServerStats = fs.statSync(`${hooksDir}/${MCP_SERVER.name}.cjs`);
|
|
console.log(`✓ mcp-server built (${(mcpServerStats.size / 1024).toFixed(2)} KB)`);
|
|
|
|
const mcpBundleContent = fs.readFileSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 'utf-8');
|
|
const bunRequireRegex = /require\(\s*["']bun:[a-z][a-z0-9_-]*["']\s*\)/;
|
|
const bunRequireMatch = mcpBundleContent.match(bunRequireRegex);
|
|
if (bunRequireMatch) {
|
|
throw new Error(
|
|
`mcp-server.cjs contains a Bun-only ${bunRequireMatch[0]} call. This means a transitive import in src/servers/mcp-server.ts pulled in code from worker-service.ts (or another module that touches DatabaseManager/ChromaSync). The MCP server runs under Node and cannot load bun:* modules. Audit recent imports in src/servers/mcp-server.ts and src/services/worker-spawner.ts — the spawner module is intentionally lightweight and MUST NOT import anything that touches SQLite or other Bun-only modules. See PR #1645 for context.`
|
|
);
|
|
}
|
|
const zodRequireRegex = /require\(\s*["']zod(?:\/[^"']*)?["']\s*\)/;
|
|
const zodRequireMatch = mcpBundleContent.match(zodRequireRegex);
|
|
if (zodRequireMatch) {
|
|
throw new Error(
|
|
`mcp-server.cjs contains external ${zodRequireMatch[0]}. Claude Desktop can launch this bundle without plugin node_modules available, so Zod must be bundled into the MCP server.`
|
|
);
|
|
}
|
|
|
|
const MCP_SERVER_MAX_BYTES = 600 * 1024;
|
|
if (mcpServerStats.size > MCP_SERVER_MAX_BYTES) {
|
|
throw new Error(
|
|
`mcp-server.cjs is ${(mcpServerStats.size / 1024).toFixed(2)} KB, exceeding the ${(MCP_SERVER_MAX_BYTES / 1024).toFixed(0)} KB budget. This usually means a transitive import pulled worker-service.ts (or another heavy module) into the MCP bundle. The MCP server is supposed to be a thin HTTP wrapper — audit recent imports in src/servers/mcp-server.ts and src/services/worker-spawner.ts. See PR #1645 for context on why this guardrail exists.`
|
|
);
|
|
}
|
|
|
|
console.log(`\n🔧 Building context generator...`);
|
|
await build({
|
|
entryPoints: [CONTEXT_GENERATOR.source],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node18',
|
|
format: 'cjs',
|
|
outfile: `${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`,
|
|
minify: true,
|
|
logLevel: 'error',
|
|
external: ['bun:sqlite', 'zod'],
|
|
define: {
|
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
|
},
|
|
// No banner needed: CJS files under Node.js have __dirname/__filename natively
|
|
});
|
|
|
|
stripHardcodedDirname(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
|
|
|
|
const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
|
|
console.log(`✓ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`);
|
|
|
|
console.log(`\n🔧 Building NPX CLI...`);
|
|
const npxCliOutDir = 'dist/npx-cli';
|
|
if (!fs.existsSync(npxCliOutDir)) {
|
|
fs.mkdirSync(npxCliOutDir, { recursive: true });
|
|
}
|
|
await build({
|
|
entryPoints: ['src/npx-cli/index.ts'],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node18',
|
|
format: 'esm',
|
|
outfile: `${npxCliOutDir}/index.js`,
|
|
banner: { js: '#!/usr/bin/env node' },
|
|
minify: true,
|
|
logLevel: 'error',
|
|
external: [
|
|
'fs', 'fs/promises', 'path', 'os', 'child_process', 'url',
|
|
'crypto', 'http', 'https', 'net', 'stream', 'util', 'events',
|
|
'buffer', 'querystring', 'readline', 'tty', 'assert',
|
|
'bun:sqlite',
|
|
],
|
|
define: {
|
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
|
},
|
|
});
|
|
|
|
fs.chmodSync(`${npxCliOutDir}/index.js`, 0o755);
|
|
const npxCliStats = fs.statSync(`${npxCliOutDir}/index.js`);
|
|
console.log(`✓ npx-cli built (${(npxCliStats.size / 1024).toFixed(2)} KB)`);
|
|
|
|
if (fs.existsSync('openclaw/src/index.ts')) {
|
|
console.log(`\n🔧 Building OpenClaw plugin...`);
|
|
const openclawOutDir = 'openclaw/dist';
|
|
if (!fs.existsSync(openclawOutDir)) {
|
|
fs.mkdirSync(openclawOutDir, { recursive: true });
|
|
}
|
|
await build({
|
|
entryPoints: ['openclaw/src/index.ts'],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node18',
|
|
format: 'esm',
|
|
outfile: `${openclawOutDir}/index.js`,
|
|
minify: true,
|
|
logLevel: 'error',
|
|
external: [
|
|
'fs', 'fs/promises', 'path', 'os', 'child_process', 'url',
|
|
'crypto', 'http', 'https', 'net', 'stream', 'util', 'events',
|
|
],
|
|
});
|
|
|
|
const openclawStats = fs.statSync(`${openclawOutDir}/index.js`);
|
|
console.log(`✓ openclaw plugin built (${(openclawStats.size / 1024).toFixed(2)} KB)`);
|
|
}
|
|
|
|
if (fs.existsSync('src/integrations/opencode-plugin/index.ts')) {
|
|
console.log(`\n🔧 Building OpenCode plugin...`);
|
|
const opencodeOutDir = 'dist/opencode-plugin';
|
|
if (!fs.existsSync(opencodeOutDir)) {
|
|
fs.mkdirSync(opencodeOutDir, { recursive: true });
|
|
}
|
|
await build({
|
|
entryPoints: ['src/integrations/opencode-plugin/index.ts'],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node18',
|
|
format: 'esm',
|
|
outfile: `${opencodeOutDir}/index.js`,
|
|
minify: true,
|
|
logLevel: 'error',
|
|
external: [
|
|
'fs', 'fs/promises', 'path', 'os', 'child_process', 'url',
|
|
'crypto', 'http', 'https', 'net', 'stream', 'util', 'events',
|
|
],
|
|
});
|
|
|
|
const opencodeStats = fs.statSync(`${opencodeOutDir}/index.js`);
|
|
console.log(`✓ opencode plugin built (${(opencodeStats.size / 1024).toFixed(2)} KB)`);
|
|
}
|
|
|
|
console.log('\n📋 Copying onboarding explainer to plugin tree...');
|
|
const onboardingExplainerSrc = 'src/services/worker/onboarding-explainer.md';
|
|
const onboardingExplainerDst = 'plugin/skills/how-it-works/onboarding-explainer.md';
|
|
if (!fs.existsSync(onboardingExplainerSrc)) {
|
|
throw new Error(`Missing onboarding explainer source: ${onboardingExplainerSrc}`);
|
|
}
|
|
fs.mkdirSync(path.dirname(onboardingExplainerDst), { recursive: true });
|
|
fs.copyFileSync(onboardingExplainerSrc, onboardingExplainerDst);
|
|
console.log(`✓ Copied ${onboardingExplainerSrc} → ${onboardingExplainerDst}`);
|
|
|
|
console.log('\n📋 Verifying distribution files...');
|
|
const validCodexHookEvents = new Set([
|
|
'SessionStart',
|
|
'UserPromptSubmit',
|
|
'PreToolUse',
|
|
'PermissionRequest',
|
|
'PostToolUse',
|
|
'Stop',
|
|
]);
|
|
const requiredDistributionFiles = [
|
|
'plugin/skills/mem-search/SKILL.md',
|
|
'plugin/skills/smart-explore/SKILL.md',
|
|
'plugin/skills/how-it-works/SKILL.md',
|
|
'plugin/skills/how-it-works/onboarding-explainer.md',
|
|
'plugin/hooks/hooks.json',
|
|
'plugin/hooks/codex-hooks.json',
|
|
'plugin/scripts/bun-runner.js',
|
|
'plugin/.claude-plugin/plugin.json',
|
|
'plugin/.codex-plugin/plugin.json',
|
|
'plugin/.mcp.json',
|
|
'.codex-plugin/plugin.json',
|
|
'.mcp.json',
|
|
'.agents/plugins/marketplace.json',
|
|
];
|
|
for (const filePath of requiredDistributionFiles) {
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error(`Missing required distribution file: ${filePath}`);
|
|
}
|
|
}
|
|
const codexHooks = JSON.parse(fs.readFileSync('plugin/hooks/codex-hooks.json', 'utf-8'));
|
|
for (const eventName of Object.keys(codexHooks.hooks ?? {})) {
|
|
if (!validCodexHookEvents.has(eventName)) {
|
|
throw new Error(`plugin/hooks/codex-hooks.json contains unknown Codex hook event: ${eventName}`);
|
|
}
|
|
}
|
|
const codexMarketplace = JSON.parse(fs.readFileSync('.agents/plugins/marketplace.json', 'utf-8'));
|
|
const claudeMemMarketplaceEntry = (codexMarketplace.plugins ?? []).find((plugin) => plugin.name === 'claude-mem');
|
|
if (claudeMemMarketplaceEntry?.source?.path !== './plugin') {
|
|
throw new Error('.agents/plugins/marketplace.json must point claude-mem source.path at ./plugin so Codex loads the bundled plugin root');
|
|
}
|
|
const rootMcp = JSON.parse(fs.readFileSync('.mcp.json', 'utf-8'));
|
|
const bundledMcp = JSON.parse(fs.readFileSync('plugin/.mcp.json', 'utf-8'));
|
|
if (JSON.stringify(rootMcp.mcpServers?.['mcp-search']) !== JSON.stringify(bundledMcp.mcpServers?.['mcp-search'])) {
|
|
throw new Error('.mcp.json and plugin/.mcp.json mcp-search launchers must stay in sync');
|
|
}
|
|
const mcpSearchCommand = bundledMcp.mcpServers?.['mcp-search']?.args?.join(' ') ?? '';
|
|
if (!mcpSearchCommand.includes('.codex/plugins/cache/claude-mem-local/claude-mem')) {
|
|
throw new Error('plugin/.mcp.json mcp-search launcher must include Codex cache fallback for hosts that do not inject PLUGIN_ROOT');
|
|
}
|
|
if (!mcpSearchCommand.includes('plugins/cache/thedotmack/claude-mem')) {
|
|
throw new Error('plugin/.mcp.json mcp-search launcher must include Claude cache fallback for hosts that do not inject PLUGIN_ROOT');
|
|
}
|
|
console.log('✓ All required distribution files present');
|
|
|
|
console.log('\n✅ All build targets compiled successfully!');
|
|
console.log(` Output: ${hooksDir}/`);
|
|
console.log(` - Worker: worker-service.cjs`);
|
|
console.log(` - Server beta: server-beta-service.cjs`);
|
|
console.log(` - MCP Server: mcp-server.cjs`);
|
|
console.log(` - Context Generator: context-generator.cjs`);
|
|
console.log(` Output: ${npxCliOutDir}/`);
|
|
console.log(` - NPX CLI: index.js`);
|
|
if (fs.existsSync('openclaw/dist/index.js')) {
|
|
console.log(` Output: openclaw/dist/`);
|
|
console.log(` - OpenClaw Plugin: index.js`);
|
|
}
|
|
if (fs.existsSync('dist/opencode-plugin/index.js')) {
|
|
console.log(` Output: dist/opencode-plugin/`);
|
|
console.log(` - OpenCode Plugin: index.js`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Build failed:', error.message);
|
|
if (error.errors) {
|
|
console.error('\nBuild errors:');
|
|
error.errors.forEach(err => console.error(` - ${err.text}`));
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
buildHooks();
|