pty-manager 1.7.2 → 1.8.0

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.
@@ -1223,24 +1223,71 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
1223
1223
  throw new Error("Session not started");
1224
1224
  }
1225
1225
  const keyList = Array.isArray(keys) ? keys : [keys];
1226
+ const normalized = _PTYSession.normalizeKeyList(keyList);
1226
1227
  this._stallEmissionCount = 0;
1227
1228
  this.resetStallTimer();
1228
- for (const key of keyList) {
1229
- const normalizedKey = key.toLowerCase().trim();
1230
- const sequence = SPECIAL_KEYS[normalizedKey];
1229
+ for (const key of normalized) {
1230
+ const sequence = SPECIAL_KEYS[key];
1231
1231
  if (sequence) {
1232
1232
  this._lastActivityAt = /* @__PURE__ */ new Date();
1233
1233
  this.ptyProcess.write(sequence);
1234
- this.logger.debug({ sessionId: this.id, key: normalizedKey }, "Sent special key");
1234
+ this.logger.debug({ sessionId: this.id, key }, "Sent special key");
1235
1235
  } else {
1236
1236
  this.logger.warn(
1237
- { sessionId: this.id, key: normalizedKey },
1237
+ { sessionId: this.id, key },
1238
1238
  "Unknown special key, sending as literal"
1239
1239
  );
1240
1240
  this.ptyProcess.write(key);
1241
1241
  }
1242
1242
  }
1243
1243
  }
1244
+ /**
1245
+ * Normalize a list of key names for SPECIAL_KEYS lookup.
1246
+ *
1247
+ * Handles two problems:
1248
+ * 1. Modifier aliases: "control" → "ctrl", "command" → "meta", "option" → "alt"
1249
+ * 2. Comma-separated compound keys from stall classifier: ["control", "c"] → ["ctrl+c"]
1250
+ * A bare modifier followed by a single char/key is joined with "+".
1251
+ */
1252
+ static normalizeKeyList(keys) {
1253
+ const MODIFIER_MAP = {
1254
+ control: "ctrl",
1255
+ command: "meta",
1256
+ cmd: "meta",
1257
+ option: "alt",
1258
+ opt: "alt"
1259
+ };
1260
+ const MODIFIER_NAMES = /* @__PURE__ */ new Set([
1261
+ "ctrl",
1262
+ "alt",
1263
+ "shift",
1264
+ "meta",
1265
+ // Also match the aliases so we can detect them before remapping
1266
+ ...Object.keys(MODIFIER_MAP)
1267
+ ]);
1268
+ const result = [];
1269
+ let i = 0;
1270
+ while (i < keys.length) {
1271
+ let key = keys[i].toLowerCase().trim();
1272
+ if (MODIFIER_MAP[key]) {
1273
+ key = MODIFIER_MAP[key];
1274
+ }
1275
+ if (MODIFIER_NAMES.has(key) && i + 1 < keys.length) {
1276
+ let nextKey = keys[i + 1].toLowerCase().trim();
1277
+ if (MODIFIER_MAP[nextKey]) {
1278
+ nextKey = MODIFIER_MAP[nextKey];
1279
+ }
1280
+ if (!MODIFIER_NAMES.has(nextKey)) {
1281
+ result.push(`${key}+${nextKey}`);
1282
+ i += 2;
1283
+ continue;
1284
+ }
1285
+ }
1286
+ result.push(key);
1287
+ i++;
1288
+ }
1289
+ return result;
1290
+ }
1244
1291
  /**
1245
1292
  * Select a TUI menu option by index (0-based).
1246
1293
  * Sends Down arrow `optionIndex` times, then Enter, with 50ms delays.
@@ -1721,7 +1768,18 @@ var ShellAdapter = class {
1721
1768
  return { detected: false };
1722
1769
  }
1723
1770
  detectReady(output) {
1724
- return output.includes(this.promptStr) || output.includes("$") || output.length > 10;
1771
+ if (this.isContinuationPrompt(output)) {
1772
+ return false;
1773
+ }
1774
+ return this.getPromptPattern().test(this.stripAnsi(output));
1775
+ }
1776
+ /**
1777
+ * Detect shell continuation prompts that indicate the shell is NOT ready
1778
+ * for a new command (e.g., unclosed quote, heredoc, backtick).
1779
+ */
1780
+ isContinuationPrompt(output) {
1781
+ const stripped = this.stripAnsi(output);
1782
+ return /(?:quote|dquote|heredoc|bquote|cmdsubst|pipe|then|else|do|loop)>\s*$/.test(stripped) || /(?:quote|dquote|heredoc|bquote)>\s*$/m.test(stripped);
1725
1783
  }
1726
1784
  detectExit(output) {
1727
1785
  if (output.includes("exit")) {
@@ -1744,7 +1802,7 @@ var ShellAdapter = class {
1744
1802
  }
1745
1803
  getPromptPattern() {
1746
1804
  const escaped = this.promptStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1747
- return new RegExp(`(?:${escaped}|\\$|#|>)\\s*$`, "m");
1805
+ return new RegExp(`(?:${escaped}|\\$|#)\\s*$`, "m");
1748
1806
  }
1749
1807
  async validateInstallation() {
1750
1808
  return { installed: true };
@@ -1892,6 +1950,19 @@ function handleSendKeys(id, keys) {
1892
1950
  ack("sendKeys", id, false, err instanceof Error ? err.message : String(err));
1893
1951
  }
1894
1952
  }
1953
+ function handleWriteRaw(id, data) {
1954
+ try {
1955
+ const session = manager.getSession(id);
1956
+ if (!session) {
1957
+ ack("writeRaw", id, false, `Session ${id} not found`);
1958
+ return;
1959
+ }
1960
+ session.writeRaw(data);
1961
+ ack("writeRaw", id, true);
1962
+ } catch (err) {
1963
+ ack("writeRaw", id, false, err instanceof Error ? err.message : String(err));
1964
+ }
1965
+ }
1895
1966
  function handlePaste(id, text, bracketed = true) {
1896
1967
  try {
1897
1968
  const session = manager.getSession(id);
@@ -2104,6 +2175,13 @@ function processCommand(line) {
2104
2175
  }
2105
2176
  handleSendKeys(command.id, command.keys);
2106
2177
  break;
2178
+ case "writeRaw":
2179
+ if (!command.id || command.data === void 0) {
2180
+ ack("writeRaw", command.id, false, "Missing id or data");
2181
+ return;
2182
+ }
2183
+ handleWriteRaw(command.id, command.data);
2184
+ break;
2107
2185
  case "paste":
2108
2186
  if (!command.id || command.text === void 0) {
2109
2187
  ack("paste", command.id, false, "Missing id or text");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pty-manager",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "PTY session manager with lifecycle management, pluggable adapters, and blocking prompt detection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",