smart-terminal-mcp 1.2.11 → 1.2.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-terminal-mcp",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "description": "MCP PTY server providing AI agents with real interactive terminal access",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -116,6 +116,9 @@ export async function runCommand({
116
116
 
117
117
  if (signal) result.signal = signal;
118
118
  if (maxOutputExceeded) result.maxOutputExceeded = true;
119
+ if (successExitCode === null && exitCode !== 0 && exitCode !== null) {
120
+ result.exitCodeIgnored = true;
121
+ }
119
122
  if (shouldIncludeSuccessChecks({ successExitCode, successFile })) {
120
123
  result.checks = checks.details;
121
124
  }
@@ -229,6 +232,14 @@ function buildSpawnPlan({ cmd, args, cwd }) {
229
232
  }
230
233
 
231
234
  const resolvedCommand = resolveWindowsCommand(cmd, cwd);
235
+
236
+ // When the caller explicitly invokes cmd.exe with /c (e.g. for shell
237
+ // built-ins like `for /f`), join the trailing args into a single verbatim
238
+ // command string so cmd.exe interprets them correctly.
239
+ if (isExplicitCmdExeCall(resolvedCommand ?? cmd, args)) {
240
+ return buildCmdExePlan(args);
241
+ }
242
+
232
243
  if (!resolvedCommand || !isWindowsBatchCommand(resolvedCommand)) {
233
244
  return {
234
245
  command: resolvedCommand ?? cmd,
@@ -244,6 +255,37 @@ function buildSpawnPlan({ cmd, args, cwd }) {
244
255
  };
245
256
  }
246
257
 
258
+ function isExplicitCmdExeCall(resolved, args) {
259
+ const comSpec = (process.env.ComSpec || 'cmd.exe').toLowerCase();
260
+ const name = resolved.toLowerCase();
261
+ if (name !== comSpec && name !== 'cmd' && name !== 'cmd.exe') return false;
262
+ return args.some((a) => a.toLowerCase() === '/c');
263
+ }
264
+
265
+ function buildCmdExePlan(args) {
266
+ const comSpec = process.env.ComSpec || 'cmd.exe';
267
+
268
+ // Collect any flags before /c (e.g. /d, /s) and the command body after /c.
269
+ const prefixFlags = [];
270
+ let commandBody = '';
271
+ let foundSlashC = false;
272
+ for (let i = 0; i < args.length; i++) {
273
+ if (!foundSlashC && args[i].toLowerCase() === '/c') {
274
+ foundSlashC = true;
275
+ // Everything after /c is the shell command – join into one string.
276
+ commandBody = args.slice(i + 1).join(' ');
277
+ break;
278
+ }
279
+ prefixFlags.push(args[i]);
280
+ }
281
+
282
+ return {
283
+ command: comSpec,
284
+ args: [...prefixFlags, '/s', '/c', `"${commandBody}"`],
285
+ windowsVerbatimArguments: true,
286
+ };
287
+ }
288
+
247
289
  function resolveWindowsCommand(cmd, cwd) {
248
290
  const pathExts = getWindowsPathExtensions();
249
291
  if (looksLikePath(cmd)) {
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { resolve as resolvePath } from 'node:path';
4
+ import { platform } from 'node:os';
4
5
  import { PtySession } from './pty-session.js';
5
- import { detectShell } from './shell-detector.js';
6
+ import { detectShell, isAvailable } from './shell-detector.js';
6
7
 
7
8
  const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
8
9
  const MAX_SESSIONS = 10;
@@ -41,7 +42,7 @@ export class SessionManager {
41
42
 
42
43
  const resolvedCwd = await resolveSessionCwd(cwd);
43
44
  const detected = detectShell();
44
- const resolvedShell = shell || detected.shell;
45
+ const resolvedShell = shell ? resolveUserShell(shell) : detected.shell;
45
46
  const shellArgs = shell ? [] : detected.args;
46
47
 
47
48
  const id = randomUUID().slice(0, 8);
@@ -121,6 +122,32 @@ export class SessionManager {
121
122
  }
122
123
  }
123
124
 
125
+ const WINDOWS_SHELL_EXTENSIONS = ['.exe', '.cmd', '.bat'];
126
+
127
+ /**
128
+ * Validate and resolve a user-provided shell name.
129
+ * On Windows, tries appending common extensions if the bare name isn't found.
130
+ * @param {string} shell
131
+ * @returns {string} The resolved shell name
132
+ */
133
+ function resolveUserShell(shell) {
134
+ if (isAvailable(shell)) return shell;
135
+
136
+ // On Windows, try appending .exe etc. so "pwsh" resolves to "pwsh.exe"
137
+ if (platform() === 'win32' && !shell.includes('.')) {
138
+ for (const ext of WINDOWS_SHELL_EXTENSIONS) {
139
+ const candidate = `${shell}${ext}`;
140
+ if (isAvailable(candidate)) return candidate;
141
+ }
142
+ }
143
+
144
+ const detected = detectShell();
145
+ throw new Error(
146
+ `Shell "${shell}" not found. The auto-detected shell is "${detected.shell}". ` +
147
+ 'Omit the shell parameter to use auto-detection, or provide the full path to the shell executable.',
148
+ );
149
+ }
150
+
124
151
  export async function resolveSessionCwd(cwd) {
125
152
  const resolvedCwd = resolvePath(cwd ?? process.cwd());
126
153
 
@@ -6,7 +6,7 @@ import { platform } from 'node:os';
6
6
  * @param {string} exe
7
7
  * @returns {boolean}
8
8
  */
9
- function isAvailable(exe) {
9
+ export function isAvailable(exe) {
10
10
  try {
11
11
  const cmd = platform() === 'win32' ? 'where' : 'which';
12
12
  execFileSync(cmd, [exe], { stdio: 'ignore', timeout: 5000 });
@@ -42,6 +42,31 @@ test('SessionManager.create rejects invalid cwd before creating a session', asyn
42
42
  }
43
43
  });
44
44
 
45
+ test('SessionManager.create rejects unknown shell before creating a session', async () => {
46
+ let constructed = 0;
47
+ class FakeSession {
48
+ constructor() {
49
+ constructed++;
50
+ }
51
+ }
52
+
53
+ const manager = new SessionManager({ SessionClass: FakeSession });
54
+
55
+ try {
56
+ await assert.rejects(
57
+ () => manager.create({ shell: 'nonexistent-shell-abc123' }),
58
+ (error) => {
59
+ assert.match(error.message, /Shell "nonexistent-shell-abc123" not found/);
60
+ return true;
61
+ }
62
+ );
63
+ assert.equal(constructed, 0);
64
+ assert.equal(manager.list().length, 0);
65
+ } finally {
66
+ manager.destroyAll();
67
+ }
68
+ });
69
+
45
70
  test('SessionManager.create rejects file cwd values', async () => {
46
71
  const tempDir = await mkdtemp(join(tmpdir(), 'smart-terminal-mcp-'));
47
72
  const filePath = join(tempDir, 'not-a-directory.txt');