@priyanshumit/macos-terminal-mcp 0.5.1 → 0.6.1

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/README.md CHANGED
@@ -14,7 +14,7 @@ A local MCP server that lets AI agents inspect and drive your macOS Terminal.app
14
14
 
15
15
  ## What it does
16
16
 
17
- Twelve MCP tools across three categories:
17
+ Fifteen MCP tools across four categories:
18
18
 
19
19
  ### Terminal interaction
20
20
 
@@ -25,6 +25,8 @@ Twelve MCP tools across three categories:
25
25
  | `terminal_execute` | **write** | Type a command into a specific tab and press Enter. Refuses if the target tab is busy unless `force=true`. `dry_run=true` returns the safety verdict without any side effects. |
26
26
  | `terminal_clear` | **write** | Wipe scrollback of a specific tab via Cmd+K. Briefly steals focus. |
27
27
  | `terminal_new_tab` | **write** | Open a new empty tab in Terminal.app and return its tty for follow-up calls. No dialog — low blast radius (user can close the tab). |
28
+ | `terminal_close_tab` | **write** | Close a specific tab by tty. Refuses busy tabs unless `force=true`. No dialog. |
29
+ | `terminal_wait_for_idle` | read | Block until the target tab is no longer busy, or until `timeout_seconds` (default 60, max 600). Polls every 250ms inside a single JXA call. |
28
30
 
29
31
  ### Safety policy management
30
32
 
@@ -43,6 +45,12 @@ Twelve MCP tools across three categories:
43
45
  | `pending_approve` | **write** | Approve a queued command by id. Triggers its own confirmation dialog. |
44
46
  | `pending_deny` | **write** | Deny a queued command. |
45
47
 
48
+ ### Observability
49
+
50
+ | Tool | Read/Write | Description |
51
+ |---|---|---|
52
+ | `audit_log_tail` | read | Read the last N entries from the audit log. Default count 20, max 1000. Returns parsed JSON array of `{timestamp, tool, outcome, ...}` entries. |
53
+
46
54
  Write tools are **off by default**. Set `WRITE_TOOLS_ENABLED=1` in the server's environment to enable them. Even when enabled, every non-`safe` operation triggers a native macOS confirmation dialog.
47
55
 
48
56
  ## Prerequisites
@@ -193,6 +201,25 @@ Whichever path resolves first wins. For solo desktop use, the dialog is the cano
193
201
 
194
202
  Pending entries auto-expire after 10 minutes. The queue is in-memory only — a server restart drops all pending entries.
195
203
 
204
+ ## Privacy & data
205
+
206
+ **No network, no telemetry.** The server speaks MCP over stdio to your local Claude client. It makes zero outbound network calls — no analytics, no crash reporting, no auto-update pings.
207
+
208
+ **What is stored on disk:**
209
+
210
+ | File | Path | Contents | Permissions |
211
+ |---|---|---|---|
212
+ | Audit log | `~/.local/state/macos-terminal-mcp/audit.log` | One JSON line per write-tool call: tool name, tty, **full command text**, safety level, matched pattern, outcome, timestamp | `0o600` (owner read/write only); parent dir `0o700` |
213
+ | Safety config | `~/.config/macos-terminal-mcp/safety.json` | User-added regex patterns with their levels and descriptions | default umask |
214
+
215
+ **Important caveat for secrets handling**: `terminal_execute` writes the **full command text** to the audit log. If a command contains a password, API key, or other secret, that secret ends up on disk in plaintext (owner-readable only). Mitigations:
216
+
217
+ - Delete the log at any time: `rm ~/.local/state/macos-terminal-mcp/audit.log`
218
+ - Default forbidden patterns already block common secret-leak vectors (`>\s*/etc/`, `\bcurl\b[^|;]*\|`, `\|\s*(bash|sh|zsh)\b`, `~/.ssh`, `/etc/passwd`, `/etc/shadow`)
219
+ - Add your own forbidden patterns for company-specific secret formats via `safety_add`
220
+
221
+ **Where the audit log helps**: post-hoc review of what an AI agent ran on your machine, debugging unexpected command refusals, and satisfying audit requirements in regulated environments.
222
+
196
223
  ## Usage examples
197
224
 
198
225
  Once registered, from Claude Code:
package/dist/index.js CHANGED
@@ -1,8 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
7
  import { registerAll } from "./tools/register.js";
5
- const server = new McpServer({ name: "macos-terminal-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
8
+ // Read package.json at runtime so serverInfo.version stays in sync with the
9
+ // shipped package. Works in dev (tsx src/index.ts → ../package.json) and from
10
+ // the installed npm package (dist/index.js → ../package.json).
11
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
12
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
13
+ const server = new McpServer({ name: "macos-terminal-mcp", version: pkg.version }, { capabilities: { tools: {} } });
6
14
  registerAll(server);
7
15
  async function main() {
8
16
  const transport = new StdioServerTransport();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,EAChD,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;AAEF,WAAW,CAAC,MAAM,CAAC,CAAC;AAEpB,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;AACvE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+BAA+B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CACpF,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,4EAA4E;AAC5E,8EAA8E;AAC9E,+DAA+D;AAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;AACpF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAwB,CAAC;AAE7E,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,EACpD,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;AAEF,WAAW,CAAC,MAAM,CAAC,CAAC;AAEpB,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;AACvE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+BAA+B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CACpF,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { appendFile, chmod, mkdir } from "node:fs/promises";
1
+ import { appendFile, chmod, mkdir, readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  function resolveDefaultPath() {
@@ -41,4 +41,34 @@ export async function appendAudit(entry, path = AUDIT_LOG_PATH) {
41
41
  process.stderr.write(`[macos-terminal-mcp] audit log write failed: ${err.message}\n`);
42
42
  }
43
43
  }
44
+ /**
45
+ * Read the last N JSONL entries from the audit log. Returns [] if the file
46
+ * does not exist (no write tool calls have happened yet).
47
+ *
48
+ * Malformed lines are silently skipped — the audit log is best-effort writes
49
+ * and a corrupt entry shouldn't break the read path.
50
+ */
51
+ export async function readAuditTail(count, path = AUDIT_LOG_PATH) {
52
+ let content;
53
+ try {
54
+ content = await readFile(path, "utf8");
55
+ }
56
+ catch (err) {
57
+ if (err.code === "ENOENT")
58
+ return [];
59
+ throw err;
60
+ }
61
+ const lines = content.split("\n").filter((l) => l.length > 0);
62
+ const tail = lines.slice(-count);
63
+ const entries = [];
64
+ for (const line of tail) {
65
+ try {
66
+ entries.push(JSON.parse(line));
67
+ }
68
+ catch {
69
+ // Defensive: skip malformed lines.
70
+ }
71
+ }
72
+ return entries;
73
+ }
44
74
  //# sourceMappingURL=audit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/safety/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAmB1C,SAAS,kBAAkB;IACzB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACvC,MAAM,IAAI,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC,IAAI,EAAE,oBAAoB,EAAE,WAAW,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,kBAAkB,EAAE,CAAC;AAEnD,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B,SAAS,QAAQ,CAAC,CAAS,EAAE,GAAW;IACtC,OAAO,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAAoC,EACpC,OAAe,cAAc;IAE7B,4EAA4E;IAC5E,uEAAuE;IACvE,0BAA0B;IAC1B,MAAM,MAAM,GAAe;QACzB,GAAG,KAAK;QACR,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,CAAC,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,4EAA4E;QAC5E,wEAAwE;QACxE,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAChD,MAAM,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gDAAiD,GAAa,CAAC,OAAO,IAAI,CAC3E,CAAC;IACJ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/safety/audit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { readAuditTail } from "../safety/audit.js";
3
+ export async function auditTailHandler({ count = 20 }) {
4
+ try {
5
+ const entries = await readAuditTail(count);
6
+ return {
7
+ content: [{ type: "text", text: JSON.stringify(entries, null, 2) }],
8
+ };
9
+ }
10
+ catch (err) {
11
+ const message = err instanceof Error ? err.message : String(err);
12
+ return {
13
+ content: [{ type: "text", text: `audit_log_tail failed: ${message}` }],
14
+ isError: true,
15
+ };
16
+ }
17
+ }
18
+ export function register(server) {
19
+ server.registerTool("audit_log_tail", {
20
+ description: "Read the last N entries from the audit log (~/.local/state/macos-terminal-mcp/audit.log, or $XDG_STATE_HOME equivalent). Each entry is a JSON object with {timestamp, tool, outcome, tty?, command?, level?, matchedPattern?, source?, errorMessage?, details?}. Read-only — does not require WRITE_TOOLS_ENABLED and is not itself logged. Returns [] if the log file does not exist yet.",
21
+ inputSchema: {
22
+ count: z
23
+ .number()
24
+ .int()
25
+ .min(1)
26
+ .max(1000)
27
+ .optional()
28
+ .describe("Number of entries to return from the tail of the log. Default 20, max 1000."),
29
+ },
30
+ annotations: { readOnlyHint: true, idempotentHint: true },
31
+ }, auditTailHandler);
32
+ }
33
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/tools/audit.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMnD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,EAAE,KAAK,GAAG,EAAE,EAAkB;IACnE,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3C,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;SACpE,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,0BAA0B,OAAO,EAAE,EAAE,CAAC;YACtE,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,WAAW,EACT,4XAA4X;QAC9X,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,IAAI,CAAC;iBACT,QAAQ,EAAE;iBACV,QAAQ,CAAC,6EAA6E,CAAC;SAC3F;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;KAC1D,EACD,gBAAgB,CACjB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ import { OsascriptError, runJxa } from "../applescript.js";
3
+ import { appendAudit } from "../safety/audit.js";
4
+ import { isWriteToolsEnabled, writeToolsDisabledMessage } from "../safety/confirm.js";
5
+ export function buildCloseTabScript(tty, force) {
6
+ return `
7
+ function safe(fn) { try { return fn(); } catch (e) { return null; } }
8
+ (function closeTab(targetTty, force) {
9
+ const terminal = Application("Terminal");
10
+ const wins = terminal.windows();
11
+ for (let wi = 0; wi < wins.length; wi++) {
12
+ const w = wins[wi];
13
+ const tabs = w.tabs();
14
+ for (let ti = 0; ti < tabs.length; ti++) {
15
+ const t = tabs[ti];
16
+ if (safe(function () { return t.tty(); }) === targetTty) {
17
+ const busy = safe(function () { return t.busy(); }) === true;
18
+ if (busy && !force) {
19
+ return JSON.stringify({ status: "busy", tty: targetTty });
20
+ }
21
+ // Terminal.app's AppleScript dictionary does NOT expose "close" on tab
22
+ // objects — only on window objects. Each tab is enumerated as its own
23
+ // "window" entry though (a quirk of the dictionary), so closing the
24
+ // enclosing "w" reference closes just this tab in the physical window.
25
+ w.close();
26
+ return JSON.stringify({ status: "closed", tty: targetTty, killedRunningCommand: busy });
27
+ }
28
+ }
29
+ }
30
+ return JSON.stringify({ status: "missing", tty: targetTty });
31
+ })(${JSON.stringify(tty)}, ${JSON.stringify(force)});
32
+ `;
33
+ }
34
+ export async function closeTabHandler({ tty, force = false, }) {
35
+ if (!isWriteToolsEnabled()) {
36
+ return {
37
+ content: [{ type: "text", text: writeToolsDisabledMessage("terminal_close_tab") }],
38
+ isError: true,
39
+ };
40
+ }
41
+ let raw;
42
+ try {
43
+ raw = await runJxa(buildCloseTabScript(tty, force));
44
+ }
45
+ catch (err) {
46
+ const message = err instanceof Error ? err.message : String(err);
47
+ const hint = err instanceof OsascriptError && /not authorized/i.test(err.stderr)
48
+ ? "\nAutomation permission missing — System Settings → Privacy & Security → Automation."
49
+ : "";
50
+ await appendAudit({
51
+ tool: "terminal_close_tab",
52
+ outcome: "error",
53
+ tty,
54
+ errorMessage: message,
55
+ });
56
+ return {
57
+ content: [{ type: "text", text: `terminal_close_tab failed: ${message}${hint}` }],
58
+ isError: true,
59
+ };
60
+ }
61
+ let result;
62
+ try {
63
+ result = JSON.parse(raw);
64
+ }
65
+ catch (err) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text: `terminal_close_tab: unexpected JXA response: ${err.message}`,
71
+ },
72
+ ],
73
+ isError: true,
74
+ };
75
+ }
76
+ if (result.status === "missing") {
77
+ return {
78
+ content: [{ type: "text", text: `No Terminal.app tab found with tty ${tty}.` }],
79
+ isError: true,
80
+ };
81
+ }
82
+ if (result.status === "busy") {
83
+ await appendAudit({
84
+ tool: "terminal_close_tab",
85
+ outcome: "refused",
86
+ tty,
87
+ details: { reason: "target tab is busy" },
88
+ });
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: `Refused: target tab ${tty} is busy with a running command. Closing it would kill the process. ` +
94
+ `Pass force=true to close anyway, or wait for the command to finish (see terminal_wait_for_idle).`,
95
+ },
96
+ ],
97
+ isError: true,
98
+ };
99
+ }
100
+ await appendAudit({
101
+ tool: "terminal_close_tab",
102
+ outcome: "success",
103
+ tty,
104
+ ...(result.killedRunningCommand
105
+ ? { details: { killedRunningCommand: true, force: true } }
106
+ : {}),
107
+ });
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: result.killedRunningCommand
113
+ ? `Closed ${tty} [force=true, killed running command].`
114
+ : `Closed ${tty}.`,
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ export function register(server) {
120
+ server.registerTool("terminal_close_tab", {
121
+ description: "Close a specific Terminal.app tab by tty. Useful for cleaning up scratch tabs spawned via terminal_new_tab. If the tab has a running foreground command, the call refuses by default — pass force=true to close anyway (the running command is killed). Requires WRITE_TOOLS_ENABLED=1 but no confirmation dialog (low blast radius for idle tabs; force=true scenarios are deliberate).",
122
+ inputSchema: {
123
+ tty: z
124
+ .string()
125
+ .regex(/^\/dev\/ttys[0-9]+$/)
126
+ .describe('Target tab tty, e.g. "/dev/ttys003"'),
127
+ force: z
128
+ .boolean()
129
+ .optional()
130
+ .describe("If true, close even when a foreground command is running (killing it). Default: false."),
131
+ },
132
+ }, closeTabHandler);
133
+ }
134
+ //# sourceMappingURL=close_tab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"close_tab.js","sourceRoot":"","sources":["../../src/tools/close_tab.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEtF,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAE,KAAc;IAC7D,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;KAyBJ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;CACjD,CAAC;AACF,CAAC;AAcD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EACpC,GAAG,EACH,KAAK,GAAG,KAAK,GACC;IACd,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;QAC3B,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,CAAC,oBAAoB,CAAC,EAAE,CAAC;YAClF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,GACR,GAAG,YAAY,cAAc,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YACjE,CAAC,CAAC,sFAAsF;YACxF,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,WAAW,CAAC;YAChB,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,OAAO;YAChB,GAAG;YACH,YAAY,EAAE,OAAO;SACtB,CAAC,CAAC;QACH,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,8BAA8B,OAAO,GAAG,IAAI,EAAE,EAAE,CAAC;YACjF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,MAAmB,CAAC;IACxB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,gDAAiD,GAAa,CAAC,OAAO,EAAE;iBAC/E;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sCAAsC,GAAG,GAAG,EAAE,CAAC;YAC/E,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC7B,MAAM,WAAW,CAAC;YAChB,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,SAAS;YAClB,GAAG;YACH,OAAO,EAAE,EAAE,MAAM,EAAE,oBAAoB,EAAE;SAC1C,CAAC,CAAC;QACH,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EACF,uBAAuB,GAAG,sEAAsE;wBAChG,kGAAkG;iBACrG;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,CAAC;QAChB,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,SAAS;QAClB,GAAG;QACH,GAAG,CAAC,MAAM,CAAC,oBAAoB;YAC7B,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC1D,CAAC,CAAC,EAAE,CAAC;KACR,CAAC,CAAC;IACH,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,MAAM,CAAC,oBAAoB;oBAC/B,CAAC,CAAC,UAAU,GAAG,wCAAwC;oBACvD,CAAC,CAAC,UAAU,GAAG,GAAG;aACrB;SACF;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,WAAW,EACT,0XAA0X;QAC5X,WAAW,EAAE;YACX,GAAG,EAAE,CAAC;iBACH,MAAM,EAAE;iBACR,KAAK,CAAC,qBAAqB,CAAC;iBAC5B,QAAQ,CAAC,qCAAqC,CAAC;YAClD,KAAK,EAAE,CAAC;iBACL,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,QAAQ,CACP,wFAAwF,CACzF;SACJ;KACF,EACD,eAAe,CAChB,CAAC;AACJ,CAAC"}
@@ -22,7 +22,38 @@ function snapshotTabs(terminal) {
22
22
  (function newTab() {
23
23
  const terminal = Application("Terminal");
24
24
  const systemEvents = Application("System Events");
25
+ // Detect cold start: if Terminal isn't running yet, activate() has to LAUNCH
26
+ // it, which takes much longer than the warm-path 150ms delay. We then wait
27
+ // for the new process to be ready before sending the keystroke.
28
+ const wasRunning = (function () { try { return terminal.running(); } catch (e) { return true; } })();
25
29
  terminal.activate();
30
+ // activate() returns before the window server actually makes Terminal frontmost,
31
+ // and macOS sometimes refuses to honor activate() when another app is in use
32
+ // by the user. System Events accessibility-layer "frontmost = true" is a more
33
+ // forceful way to make Terminal the key app — required for Cmd+T to land on it.
34
+ try { Application("System Events").applicationProcesses["Terminal"].frontmost = true; } catch (e) { /* best-effort */ }
35
+ // Cold start: poll up to 3s for Terminal to come up. Warm start: short settle.
36
+ if (!wasRunning) {
37
+ for (let i = 0; i < 30; i++) {
38
+ delay(0.1);
39
+ try { if (terminal.running()) break; } catch (e) { /* keep waiting */ }
40
+ }
41
+ delay(0.5);
42
+ // Cold-start: Terminal almost always auto-opens a default window per the
43
+ // user's "When Terminal Starts" preference. Since Terminal wasn't running
44
+ // before, ANY tab that exists now is the new one we owe the caller — return
45
+ // it directly. Don't ALSO send Cmd+T (which would create a second tab the
46
+ // caller didn't ask for; reported by user during v0.6.1 verification).
47
+ const afterLaunch = snapshotTabs(terminal);
48
+ const launchedKeys = Object.keys(afterLaunch);
49
+ if (launchedKeys.length > 0) {
50
+ return JSON.stringify({ tty: launchedKeys[0], windowId: afterLaunch[launchedKeys[0]] });
51
+ }
52
+ // Cold launch produced no default window (user disabled it in prefs) — fall
53
+ // through to the keystroke path so Cmd+N creates one.
54
+ } else {
55
+ delay(0.3);
56
+ }
26
57
  const before = snapshotTabs(terminal);
27
58
  const wasEmpty = Object.keys(before).length === 0;
28
59
  // Cmd+T opens a new tab in the front window; Cmd+N opens a new window when
@@ -1 +1 @@
1
- {"version":3,"file":"new_tab.js","sourceRoot":"","sources":["../../src/tools/new_tab.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEtF,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgD7B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;QAC3B,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAChF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QAC1C,MAAM,WAAW,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACpE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,GACR,GAAG,YAAY,cAAc,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YACjE,CAAC,CAAC,sFAAsF;YACxF,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,WAAW,CAAC;YAChB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,OAAO;YAChB,YAAY,EAAE,OAAO;SACtB,CAAC,CAAC;QACH,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,4BAA4B,OAAO,GAAG,IAAI,EAAE,EAAE,CAAC;YAC/E,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,WAAW,EACT,2nBAA2nB;KAC9nB,EACD,aAAa,CACd,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"new_tab.js","sourceRoot":"","sources":["../../src/tools/new_tab.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEtF,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+E7B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;QAC3B,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAChF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QAC1C,MAAM,WAAW,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACpE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,GACR,GAAG,YAAY,cAAc,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YACjE,CAAC,CAAC,sFAAsF;YACxF,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,WAAW,CAAC;YAChB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,OAAO;YAChB,YAAY,EAAE,OAAO;SACtB,CAAC,CAAC;QACH,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,4BAA4B,OAAO,GAAG,IAAI,EAAE,EAAE,CAAC;YAC/E,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,WAAW,EACT,2nBAA2nB;KAC9nB,EACD,aAAa,CACd,CAAC;AACJ,CAAC"}
@@ -1,17 +1,27 @@
1
+ import * as audit from "./audit.js";
1
2
  import * as clear from "./clear.js";
3
+ import * as closeTab from "./close_tab.js";
2
4
  import * as execute from "./execute.js";
3
5
  import * as list from "./list.js";
4
6
  import * as newTab from "./new_tab.js";
5
7
  import * as pending from "./pending.js";
6
8
  import * as read from "./read.js";
7
9
  import * as safety from "./safety.js";
10
+ import * as waitForIdle from "./wait_for_idle.js";
8
11
  export function registerAll(server) {
12
+ // Terminal interaction
9
13
  list.register(server);
10
14
  read.register(server);
11
15
  execute.register(server);
12
16
  clear.register(server);
13
17
  newTab.register(server);
18
+ closeTab.register(server);
19
+ waitForIdle.register(server);
20
+ // Safety policy
14
21
  safety.register(server);
22
+ // Async approval queue
15
23
  pending.register(server);
24
+ // Observability
25
+ audit.register(server);
16
26
  }
17
27
  //# sourceMappingURL=register.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/tools/register.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,UAAU,WAAW,CAAC,MAAiB;IAC3C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC"}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/tools/register.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,WAAW,MAAM,oBAAoB,CAAC;AAElD,MAAM,UAAU,WAAW,CAAC,MAAiB;IAC3C,uBAAuB;IACvB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC7B,gBAAgB;IAChB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxB,uBAAuB;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,gBAAgB;IAChB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { runJxa } from "../applescript.js";
3
+ const DEFAULT_TIMEOUT_SEC = 60;
4
+ const MAX_TIMEOUT_SEC = 600;
5
+ const POLL_INTERVAL_SEC = 0.25;
6
+ export function buildWaitForIdleScript(tty, timeoutSec) {
7
+ return `
8
+ function safe(fn) { try { return fn(); } catch (e) { return null; } }
9
+ var app = Application.currentApplication();
10
+ app.includeStandardAdditions = true;
11
+ (function waitForIdle(targetTty, timeoutSec) {
12
+ const terminal = Application("Terminal");
13
+ const startMs = Date.now();
14
+ const deadlineMs = startMs + timeoutSec * 1000;
15
+
16
+ function probe() {
17
+ const wins = terminal.windows();
18
+ for (let wi = 0; wi < wins.length; wi++) {
19
+ const w = wins[wi];
20
+ const tabs = w.tabs();
21
+ for (let ti = 0; ti < tabs.length; ti++) {
22
+ const t = tabs[ti];
23
+ if (safe(function () { return t.tty(); }) === targetTty) {
24
+ return { found: true, busy: safe(function () { return t.busy(); }) === true };
25
+ }
26
+ }
27
+ }
28
+ return { found: false, busy: false };
29
+ }
30
+
31
+ let state = probe();
32
+ if (!state.found) {
33
+ return JSON.stringify({ tty: targetTty, missing: true, waited_ms: 0 });
34
+ }
35
+ if (!state.busy) {
36
+ return JSON.stringify({ tty: targetTty, idle: true, waited_ms: 0 });
37
+ }
38
+
39
+ while (Date.now() < deadlineMs) {
40
+ delay(${POLL_INTERVAL_SEC});
41
+ state = probe();
42
+ if (!state.found) {
43
+ return JSON.stringify({ tty: targetTty, missing: true, waited_ms: Date.now() - startMs });
44
+ }
45
+ if (!state.busy) {
46
+ return JSON.stringify({ tty: targetTty, idle: true, waited_ms: Date.now() - startMs });
47
+ }
48
+ }
49
+
50
+ return JSON.stringify({ tty: targetTty, timed_out: true, waited_ms: Date.now() - startMs });
51
+ })(${JSON.stringify(tty)}, ${timeoutSec});
52
+ `;
53
+ }
54
+ export async function waitForIdleHandler({ tty, timeout_seconds = DEFAULT_TIMEOUT_SEC, }) {
55
+ const timeoutSec = Math.min(Math.max(timeout_seconds, 1), MAX_TIMEOUT_SEC);
56
+ try {
57
+ // Outer runJxa timeout is the inner poll deadline + 5s buffer to let the
58
+ // JXA finish writing its "timed_out" result before SIGKILL engages.
59
+ const raw = await runJxa(buildWaitForIdleScript(tty, timeoutSec), {
60
+ timeoutMs: (timeoutSec + 5) * 1000,
61
+ });
62
+ return { content: [{ type: "text", text: raw }] };
63
+ }
64
+ catch (err) {
65
+ const message = err instanceof Error ? err.message : String(err);
66
+ return {
67
+ content: [{ type: "text", text: `terminal_wait_for_idle failed: ${message}` }],
68
+ isError: true,
69
+ };
70
+ }
71
+ }
72
+ export function register(server) {
73
+ server.registerTool("terminal_wait_for_idle", {
74
+ description: "Block until the target Terminal.app tab is no longer busy (no foreground command running), or until timeout. Polls every 250ms inside a single osascript invocation. Useful for sequential workflows like 'run npm install, then run npm test'. Returns one of {idle: true, waited_ms}, {timed_out: true, waited_ms}, or {missing: true, waited_ms}. Read-only — no WRITE_TOOLS_ENABLED gate.",
75
+ inputSchema: {
76
+ tty: z
77
+ .string()
78
+ .regex(/^\/dev\/ttys[0-9]+$/)
79
+ .describe('Target tab tty, e.g. "/dev/ttys003"'),
80
+ timeout_seconds: z
81
+ .number()
82
+ .int()
83
+ .min(1)
84
+ .max(MAX_TIMEOUT_SEC)
85
+ .optional()
86
+ .describe(`Maximum seconds to wait before giving up. Default ${DEFAULT_TIMEOUT_SEC}, max ${MAX_TIMEOUT_SEC}.`),
87
+ },
88
+ annotations: { readOnlyHint: true, idempotentHint: true },
89
+ }, waitForIdleHandler);
90
+ }
91
+ //# sourceMappingURL=wait_for_idle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait_for_idle.js","sourceRoot":"","sources":["../../src/tools/wait_for_idle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAC/B,MAAM,eAAe,GAAG,GAAG,CAAC;AAC5B,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAE/B,MAAM,UAAU,sBAAsB,CAAC,GAAW,EAAE,UAAkB;IACpE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAiCG,iBAAiB;;;;;;;;;;;KAWxB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,UAAU;CACtC,CAAC;AACF,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EACvC,GAAG,EACH,eAAe,GAAG,mBAAmB,GACpB;IACjB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;IAC3E,IAAI,CAAC;QACH,yEAAyE;QACzE,oEAAoE;QACpE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE;YAChE,SAAS,EAAE,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,IAAI;SACnC,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IACpD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kCAAkC,OAAO,EAAE,EAAE,CAAC;YAC9E,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,CAAC,YAAY,CACjB,wBAAwB,EACxB;QACE,WAAW,EACT,+XAA+X;QACjY,WAAW,EAAE;YACX,GAAG,EAAE,CAAC;iBACH,MAAM,EAAE;iBACR,KAAK,CAAC,qBAAqB,CAAC;iBAC5B,QAAQ,CAAC,qCAAqC,CAAC;YAClD,eAAe,EAAE,CAAC;iBACf,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,eAAe,CAAC;iBACpB,QAAQ,EAAE;iBACV,QAAQ,CACP,qDAAqD,mBAAmB,SAAS,eAAe,GAAG,CACpG;SACJ;QACD,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;KAC1D,EACD,kBAAkB,CACnB,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@priyanshumit/macos-terminal-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "MCP server exposing macOS Terminal.app to AI agents via AppleScript / JXA. List windows, read scrollback, execute commands, clear buffers — with three-tier safety patterns and confirmation dialogs.",
5
5
  "type": "module",
6
6
  "bin": {