omnikey-cli 1.0.42 → 1.0.43

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.
@@ -5,12 +5,20 @@
5
5
  // exposes their tools to the agent as `AITool` entries. The agent's tool
6
6
  // dispatcher routes any tool call whose name starts with `MCP_TOOL_PREFIX`
7
7
  // back here so it is forwarded to the originating MCP server.
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
8
11
  Object.defineProperty(exports, "__esModule", { value: true });
9
12
  exports.MCP_TOOL_PREFIX = void 0;
13
+ exports.resolveLoginShell = resolveLoginShell;
14
+ exports.wrapWithLoginShell = wrapWithLoginShell;
15
+ exports.buildStdioChildEnv = buildStdioChildEnv;
10
16
  exports.getMcpToolsForSubscription = getMcpToolsForSubscription;
11
17
  exports.executeMcpTool = executeMcpTool;
12
18
  exports.invalidateMcpRuntimeForServer = invalidateMcpRuntimeForServer;
13
19
  exports.shutdownAllMcpClients = shutdownAllMcpClients;
20
+ const fs_1 = require("fs");
21
+ const path_1 = __importDefault(require("path"));
14
22
  const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
15
23
  const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
16
24
  const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
@@ -20,6 +28,130 @@ const mcpServer_1 = require("../models/mcpServer");
20
28
  exports.MCP_TOOL_PREFIX = 'mcp_';
21
29
  const MAX_TOOL_NAME_LEN = 64;
22
30
  const CONNECT_TIMEOUT_MS = 15000;
31
+ const STDIO_STDERR_MAX_BYTES = 16 * 1024;
32
+ const STDIO_STDERR_DRAIN_MS = 2000;
33
+ // Ordered candidate list for resolving the user's login shell on Unix/macOS,
34
+ // mirroring the resolvedLoginShell() logic in the macOS terminal launch path.
35
+ // The SHELL environment variable is checked first at call-time (not here).
36
+ const UNIX_SHELL_CANDIDATES = [
37
+ // zsh — default on macOS Catalina+
38
+ '/bin/zsh',
39
+ '/usr/bin/zsh',
40
+ '/usr/local/bin/zsh', // Homebrew (Intel)
41
+ '/opt/homebrew/bin/zsh', // Homebrew (Apple Silicon)
42
+ // bash — default on older macOS / most Linux distros
43
+ '/bin/bash',
44
+ '/usr/bin/bash',
45
+ '/usr/local/bin/bash',
46
+ '/opt/homebrew/bin/bash',
47
+ // fish — popular third-party shell
48
+ '/usr/local/bin/fish',
49
+ '/opt/homebrew/bin/fish',
50
+ '/usr/bin/fish',
51
+ // ksh — KornShell, common on Linux
52
+ '/bin/ksh',
53
+ '/usr/bin/ksh',
54
+ '/usr/local/bin/ksh',
55
+ // tcsh — legacy, still present on some macOS/BSD systems
56
+ '/bin/tcsh',
57
+ '/usr/bin/tcsh',
58
+ // sh — POSIX fallback, always present
59
+ '/bin/sh',
60
+ '/usr/bin/sh',
61
+ ];
62
+ // Ordered candidate list for Windows shells. COMSPEC is checked first at call-time.
63
+ const WINDOWS_SHELL_CANDIDATES = [
64
+ // PowerShell Core (7+) — preferred for modern Windows
65
+ 'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
66
+ 'C:\\Program Files\\PowerShell\\6\\pwsh.exe',
67
+ // Windows PowerShell — built-in on all Windows versions
68
+ 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
69
+ // cmd.exe — last resort
70
+ 'C:\\Windows\\System32\\cmd.exe',
71
+ 'C:\\Windows\\cmd.exe',
72
+ ];
73
+ // Homebrew / system binary directories to prepend on Unix when the daemon's
74
+ // inherited PATH is bare (e.g. launched by launchctl). This is a belt-and-
75
+ // suspenders fallback; running through a login shell already handles this.
76
+ const UNIX_EXTRA_PATH_ENTRIES = [
77
+ '/opt/homebrew/bin',
78
+ '/opt/homebrew/sbin',
79
+ '/usr/local/bin',
80
+ '/usr/local/sbin',
81
+ ];
82
+ // Common Windows binary directories to prepend when PATHEXT / system dirs are
83
+ // missing from the inherited PATH.
84
+ const WINDOWS_EXTRA_PATH_ENTRIES = [
85
+ 'C:\\Windows\\System32',
86
+ 'C:\\Windows',
87
+ 'C:\\Windows\\System32\\Wbem',
88
+ 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0',
89
+ // Scoop (user-level package manager)
90
+ `${process.env.USERPROFILE ?? 'C:\\Users\\Default'}\\scoop\\shims`,
91
+ // Node.js user-level installer
92
+ `${process.env.APPDATA ?? 'C:\\Users\\Default\\AppData\\Roaming'}\\npm`,
93
+ ];
94
+ function isExecutable(filePath) {
95
+ try {
96
+ (0, fs_1.accessSync)(filePath, fs_1.constants.X_OK);
97
+ return true;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Resolve the login shell to use when wrapping stdio MCP child processes.
105
+ * On Unix/macOS mirrors the resolvedLoginShell() logic from the macOS terminal
106
+ * launch path: check $SHELL first, then walk a fixed candidate list.
107
+ * On Windows: check %COMSPEC%, then prefer PowerShell Core > Windows PowerShell > cmd.
108
+ */
109
+ function resolveLoginShell() {
110
+ if (process.platform === 'win32') {
111
+ const comspec = process.env.COMSPEC ?? '';
112
+ if (comspec && (0, fs_1.existsSync)(comspec))
113
+ return comspec;
114
+ for (const candidate of WINDOWS_SHELL_CANDIDATES) {
115
+ if ((0, fs_1.existsSync)(candidate))
116
+ return candidate;
117
+ }
118
+ return 'cmd.exe';
119
+ }
120
+ const envShell = process.env.SHELL ?? '';
121
+ const candidates = envShell ? [envShell, ...UNIX_SHELL_CANDIDATES] : [...UNIX_SHELL_CANDIDATES];
122
+ for (const candidate of candidates) {
123
+ if (candidate && (0, fs_1.existsSync)(candidate) && isExecutable(candidate))
124
+ return candidate;
125
+ }
126
+ return '/bin/sh';
127
+ }
128
+ /** Single-quote a Unix shell argument, safely escaping embedded single quotes. */
129
+ function shellEscapeUnix(arg) {
130
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
131
+ }
132
+ /**
133
+ * Wrap `command args` so it is executed through the resolved login shell.
134
+ *
135
+ * On Unix: `shell -l -c 'command args...'`
136
+ * `-l` sources the login profile (.zprofile, .bash_profile, etc.) so the
137
+ * child inherits the same PATH as an interactive terminal session.
138
+ *
139
+ * On Windows (powershell / pwsh): `shell -NoProfile -Command "command args..."`
140
+ * On Windows (cmd): `shell /c "command args..."`
141
+ */
142
+ function wrapWithLoginShell(shell, command, args) {
143
+ if (process.platform === 'win32') {
144
+ const shellName = path_1.default.basename(shell).toLowerCase();
145
+ const cmdStr = [command, ...args].join(' ');
146
+ if (shellName === 'pwsh.exe' || shellName === 'powershell.exe') {
147
+ return { command: shell, args: ['-NoProfile', '-Command', cmdStr] };
148
+ }
149
+ // cmd.exe — /c runs the rest of the line as a command
150
+ return { command: shell, args: ['/c', cmdStr] };
151
+ }
152
+ const fullCmd = [command, ...args].map(shellEscapeUnix).join(' ');
153
+ return { command: shell, args: ['-l', '-c', fullCmd] };
154
+ }
23
155
  const clients = new Map(); // by MCPServer.id
24
156
  function slug(s) {
25
157
  return s
@@ -38,7 +170,54 @@ function isStdioAllowed() {
38
170
  // outbound HTTP/SSE transports are permitted.
39
171
  return config_1.config.isSelfHosted === true || config_1.config.isLocal === true;
40
172
  }
173
+ /**
174
+ * Build the environment for a spawned stdio MCP child process.
175
+ *
176
+ * Starts from the parent `process.env`, then prepends the standard
177
+ * Homebrew / `/usr/local` binary directories (Unix) or common Windows system
178
+ * directories to PATH, preserving the existing PATH after them and never
179
+ * duplicating entries already present. This is a belt-and-suspenders measure
180
+ * on top of the login-shell wrapping: the shell's `-l` flag sources the login
181
+ * profile, but augmenting PATH here also helps when the shell is not found.
182
+ * User-supplied `serverEnv` is overlaid last so an explicit PATH wins.
183
+ * The result is a strict `Record<string, string>` with any `undefined`
184
+ * values stripped out (process.env can legally contain those).
185
+ */
186
+ function buildStdioChildEnv(serverEnv) {
187
+ const base = {};
188
+ for (const [k, v] of Object.entries(process.env)) {
189
+ if (typeof v === 'string')
190
+ base[k] = v;
191
+ }
192
+ if (process.platform === 'win32') {
193
+ // On Windows, PATH lookup is case-insensitive; normalize to the 'Path' key
194
+ // that Node uses, then prepend common system directories.
195
+ const pathKey = Object.keys(base).find((k) => k.toLowerCase() === 'path') ?? 'Path';
196
+ const currentPath = base[pathKey] ?? '';
197
+ const existing = currentPath.split(';').filter((p) => p.length > 0);
198
+ const existingSet = new Set(existing.map((p) => p.toLowerCase()));
199
+ const toPrepend = WINDOWS_EXTRA_PATH_ENTRIES.filter((p) => !existingSet.has(p.toLowerCase()));
200
+ base[pathKey] = [...toPrepend, ...existing].join(';');
201
+ }
202
+ else {
203
+ const currentPath = base.PATH ?? '';
204
+ const existing = currentPath.split(':').filter((p) => p.length > 0);
205
+ const existingSet = new Set(existing);
206
+ const toPrepend = UNIX_EXTRA_PATH_ENTRIES.filter((p) => !existingSet.has(p));
207
+ base.PATH = [...toPrepend, ...existing].join(':');
208
+ }
209
+ if (serverEnv) {
210
+ for (const [k, v] of Object.entries(serverEnv)) {
211
+ if (typeof v === 'string')
212
+ base[k] = v;
213
+ }
214
+ }
215
+ return base;
216
+ }
41
217
  async function connectOne(server, log) {
218
+ // Hoisted so the catch block can drain transport.stderr for diagnostics.
219
+ let stdioTransport;
220
+ let stderrBuffer = '';
42
221
  try {
43
222
  if (server.transport === 'stdio' && !isStdioAllowed()) {
44
223
  throw new Error('stdio MCP transport is disabled in this deployment.');
@@ -47,14 +226,39 @@ async function connectOne(server, log) {
47
226
  if (server.transport === 'stdio') {
48
227
  if (!server.command)
49
228
  throw new Error('command is required for stdio transport');
50
- const transport = new stdio_js_1.StdioClientTransport({
229
+ const childEnv = buildStdioChildEnv(server.env);
230
+ // Wrap through the login shell so the child process inherits the same
231
+ // PATH as an interactive terminal session (sources .zprofile, .bash_profile,
232
+ // etc.). This is equivalent to how macOS Terminal launches a new window.
233
+ const loginShell = resolveLoginShell();
234
+ const { command: wrappedCmd, args: wrappedArgs } = wrapWithLoginShell(loginShell, server.command, server.args ?? []);
235
+ log.info('Spawning stdio MCP server', {
236
+ mcpServerId: server.id,
237
+ mcpServerName: server.name,
51
238
  command: server.command,
52
239
  args: server.args ?? [],
53
- // Pass-through the user-provided env in addition to a safe default set.
54
- env: { ...process.env, ...(server.env ?? {}) },
240
+ loginShell,
241
+ wrappedCommand: wrappedCmd,
242
+ wrappedArgs,
243
+ path: childEnv.PATH,
244
+ });
245
+ stdioTransport = new stdio_js_1.StdioClientTransport({
246
+ command: wrappedCmd,
247
+ args: wrappedArgs,
248
+ env: childEnv,
55
249
  stderr: 'pipe',
56
250
  });
57
- await withTimeout(client.connect(transport), CONNECT_TIMEOUT_MS, 'MCP stdio connect');
251
+ // Attach the stderr listener eagerly: the child can fail (e.g. `env: node:
252
+ // No such file or directory`) and close the stream before our catch block
253
+ // runs, so we have to start buffering before we await client.connect().
254
+ stdioTransport.stderr?.on('data', (chunk) => {
255
+ if (stderrBuffer.length >= STDIO_STDERR_MAX_BYTES)
256
+ return;
257
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
258
+ const remaining = STDIO_STDERR_MAX_BYTES - stderrBuffer.length;
259
+ stderrBuffer += text.length > remaining ? text.slice(0, remaining) : text;
260
+ });
261
+ await withTimeout(client.connect(stdioTransport), CONNECT_TIMEOUT_MS, 'MCP stdio connect');
58
262
  }
59
263
  else if (server.transport === 'http') {
60
264
  if (!server.url)
@@ -89,16 +293,47 @@ async function connectOne(server, log) {
89
293
  }
90
294
  catch (err) {
91
295
  const message = err instanceof Error ? err.message : String(err);
296
+ let childStderr;
297
+ if (server.transport === 'stdio' && stdioTransport) {
298
+ childStderr = await drainStdioStderr(stdioTransport, () => stderrBuffer);
299
+ }
92
300
  log.warn('Failed to connect to MCP server', {
93
301
  mcpServerId: server.id,
94
302
  mcpServerName: server.name,
95
303
  transport: server.transport,
96
304
  error: message,
305
+ ...(childStderr !== undefined ? { childStderr } : {}),
97
306
  });
98
307
  await mcpServer_1.MCPServer.update({ lastError: message }, { where: { id: server.id } }).catch(() => undefined);
99
308
  return null;
100
309
  }
101
310
  }
311
+ /**
312
+ * Return the buffered stderr from a failed stdio transport. Waits up to
313
+ * STDIO_STDERR_DRAIN_MS for the stream to emit any final chunks (the child
314
+ * process may still be flushing its stderr when we land in the catch block).
315
+ */
316
+ async function drainStdioStderr(transport, read) {
317
+ const stderr = transport.stderr;
318
+ if (!stderr)
319
+ return read().trim();
320
+ await new Promise((resolve) => {
321
+ let settled = false;
322
+ const finish = () => {
323
+ if (settled)
324
+ return;
325
+ settled = true;
326
+ clearTimeout(timer);
327
+ stderr.removeListener('end', finish);
328
+ stderr.removeListener('close', finish);
329
+ resolve();
330
+ };
331
+ const timer = setTimeout(finish, STDIO_STDERR_DRAIN_MS);
332
+ stderr.once('end', finish);
333
+ stderr.once('close', finish);
334
+ });
335
+ return read().trim();
336
+ }
102
337
  async function getOrConnect(server, log) {
103
338
  const cached = clients.get(server.id);
104
339
  if (cached)
@@ -77,8 +77,8 @@ app.get('/macos/appcast', (req, res) => {
77
77
  const appcastUrl = `${baseUrl}/macos/appcast`;
78
78
  // These should match the values embedded into the macOS app
79
79
  // Info.plist in macOS/build_release_dmg.sh.
80
- const bundleVersion = '31';
81
- const shortVersion = '1.0.30';
80
+ const bundleVersion = '33';
81
+ const shortVersion = '1.0.32';
82
82
  const xml = `<?xml version="1.0" encoding="utf-8"?>
83
83
  <rss version="2.0"
84
84
  xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
7
- "version": "1.0.42",
7
+ "version": "1.0.43",
8
8
  "description": "CLI for onboarding users to Omnikey AI and configuring OPENAI_API_KEY. Use Yarn for install/build.",
9
9
  "engines": {
10
10
  "node": ">=14.0.0",