Summary
Running any email-agent-mcp subcommand (configure, serve, status, --version) exits silently with code 0 and no output when the package is installed globally via npm link. The root cause is the isDirectRun guard in cli.js which checks process.argv[1]?.endsWith('cli.js') — but when npm-linked, process.argv[1] is the symlink name (email-agent-mcp), not cli.js.
Reproduction
# In the email-agent-mcp repo
npm link
# Anywhere
email-agent-mcp configure --provider gmail
# → exits 0, no output, no browser, nothing
email-agent-mcp --version
# → exits 0, no output
email-agent-mcp status
# → exits 0, no output
Root cause
packages/email-mcp/src/cli.ts (line ~1122):
const isDirectRun = process.argv[1]?.endsWith('cli.ts') || process.argv[1]?.endsWith('cli.js');
if (isDirectRun) {
runCli(process.argv.slice(2)).then(code => {
process.exit(code);
}).catch(err => {
console.error('Fatal:', err);
process.exit(1);
});
}
When installed via npm link, the global bin is a symlink:
~/.npm-global/bin/email-agent-mcp → ../lib/node_modules/@usejunior/email-mcp/dist/cli.js
But process.argv[1] in Node.js is the symlink path, not the resolved target:
process.argv[1] = '/Users/you/.npm-global/bin/email-agent-mcp'
'email-agent-mcp'.endsWith('cli.js') → false. So isDirectRun is false, runCli() is never called, Node loads the module exports, does nothing with them, and exits 0.
Verified by direct invocation
Calling runConfigure directly (bypassing the guard) works correctly:
import('./dist/cli.js').then(async m => {
const code = await m.runConfigure({ provider: 'gmail' });
console.log('EXIT_CODE=' + code);
});
// → prints: ❌ Configuration failed: Missing Gmail OAuth client ID...
// → EXIT_CODE=1
This confirms the configure logic itself is correct — only the entry-point guard is broken.
Suggested fix
The isDirectRun guard needs symlink-safe direct-run detection. One approach using import.meta.url (which resolves through symlinks) compared against realpathSync(process.argv[1]):
import { fileURLToPath } from 'node:url';
import { realpathSync } from 'node:fs';
const isDirectRun = (() => {
try {
const self = fileURLToPath(import.meta.url);
const invoked = realpathSync(process.argv[1] ?? '');
return self === invoked;
} catch {
return false;
}
})();
The try/catch is necessary because process.argv[1] may be missing or invalid in edge cases (process managers, node -e). The catch should fall back to false (don't auto-execute) rather than throwing.
Impact
This affects ALL users who install via npm link (common during development) or any global install where the binary name doesn't match cli.js. Every subcommand silently does nothing. The exit code 0 makes it especially dangerous — it looks like success.
Summary
Running any
email-agent-mcpsubcommand (configure,serve,status,--version) exits silently with code 0 and no output when the package is installed globally vianpm link. The root cause is theisDirectRunguard incli.jswhich checksprocess.argv[1]?.endsWith('cli.js')— but when npm-linked,process.argv[1]is the symlink name (email-agent-mcp), notcli.js.Reproduction
Root cause
packages/email-mcp/src/cli.ts(line ~1122):When installed via
npm link, the global bin is a symlink:But
process.argv[1]in Node.js is the symlink path, not the resolved target:'email-agent-mcp'.endsWith('cli.js')→false. SoisDirectRunis false,runCli()is never called, Node loads the module exports, does nothing with them, and exits 0.Verified by direct invocation
Calling
runConfiguredirectly (bypassing the guard) works correctly:This confirms the configure logic itself is correct — only the entry-point guard is broken.
Suggested fix
The
isDirectRunguard needs symlink-safe direct-run detection. One approach usingimport.meta.url(which resolves through symlinks) compared againstrealpathSync(process.argv[1]):The
try/catchis necessary becauseprocess.argv[1]may be missing or invalid in edge cases (process managers,node -e). The catch should fall back tofalse(don't auto-execute) rather than throwing.Impact
This affects ALL users who install via
npm link(common during development) or any global install where the binary name doesn't matchcli.js. Every subcommand silently does nothing. The exit code 0 makes it especially dangerous — it looks like success.