@priyanshumit/macos-terminal-mcp 0.3.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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/applescript.js +75 -0
- package/dist/applescript.js.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/safety/audit.js +30 -0
- package/dist/safety/audit.js.map +1 -0
- package/dist/safety/confirm.js +44 -0
- package/dist/safety/confirm.js.map +1 -0
- package/dist/safety/patterns.js +176 -0
- package/dist/safety/patterns.js.map +1 -0
- package/dist/safety/queue.js +70 -0
- package/dist/safety/queue.js.map +1 -0
- package/dist/tools/clear.js +100 -0
- package/dist/tools/clear.js.map +1 -0
- package/dist/tools/execute.js +158 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/list.js +52 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/pending.js +125 -0
- package/dist/tools/pending.js.map +1 -0
- package/dist/tools/read.js +64 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/register.js +15 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/safety.js +213 -0
- package/dist/tools/safety.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Priyanshu Mittal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# macos-terminal-mcp
|
|
2
|
+
|
|
3
|
+
A local MCP server that lets AI agents inspect and drive your macOS Terminal.app tabs — list windows, read scrollback, execute commands, and clear buffers, all gated by a three-tier safety model plus per-call user confirmation for any write operation.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Eleven MCP tools across three categories:
|
|
8
|
+
|
|
9
|
+
### Terminal interaction
|
|
10
|
+
|
|
11
|
+
| Tool | Read/Write | Description |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `terminal_list` | read | Enumerate every open Terminal.app tab with tty, title, busy state, and foreground processes. |
|
|
14
|
+
| `terminal_read` | read | Return the full buffer + scrollback of a specific tab, identified by tty. |
|
|
15
|
+
| `terminal_execute` | **write** | Type a command into a specific tab and press Enter. Evaluated against the safety policy; runs auto/confirms/refuses depending on level. |
|
|
16
|
+
| `terminal_clear` | **write** | Wipe scrollback of a specific tab via Cmd+K. Briefly steals focus. |
|
|
17
|
+
|
|
18
|
+
### Safety policy management
|
|
19
|
+
|
|
20
|
+
| Tool | Read/Write | Description |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `safety_list` | read | Show all current safety patterns with their levels. |
|
|
23
|
+
| `safety_add` | **write** | Propose a new pattern + level; user approves via dialog before persisting. |
|
|
24
|
+
| `safety_remove` | **write** | Drop a pattern. Warns prominently in the dialog if the pattern is `forbidden`. |
|
|
25
|
+
| `safety_set_level` | **write** | Change the level of an existing pattern. Warns when downgrading from `forbidden`. |
|
|
26
|
+
|
|
27
|
+
### Async approval queue
|
|
28
|
+
|
|
29
|
+
| Tool | Read/Write | Description |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `pending_list` | read | Snapshot of commands currently awaiting approval. |
|
|
32
|
+
| `pending_approve` | **write** | Approve a queued command by id. Triggers its own confirmation dialog. |
|
|
33
|
+
| `pending_deny` | **write** | Deny a queued command. |
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
## Prerequisites
|
|
38
|
+
|
|
39
|
+
- macOS (uses JXA / `osascript`)
|
|
40
|
+
- Node ≥ 20
|
|
41
|
+
- Terminal.app (the stock macOS terminal)
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
### Option A: via npm (recommended)
|
|
46
|
+
|
|
47
|
+
Once published:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Run directly without installing
|
|
51
|
+
npx -y @priyanshumit/macos-terminal-mcp
|
|
52
|
+
|
|
53
|
+
# Or install globally
|
|
54
|
+
npm install -g @priyanshumit/macos-terminal-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Option B: from source
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/priyanshumit/macos-terminal-mcp.git
|
|
61
|
+
cd macos-terminal-mcp
|
|
62
|
+
npm install
|
|
63
|
+
npm run build
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## macOS permissions
|
|
67
|
+
|
|
68
|
+
First time the MCP server controls Terminal.app, macOS will prompt for permission:
|
|
69
|
+
|
|
70
|
+
> *"node" wants access to control "Terminal".*
|
|
71
|
+
|
|
72
|
+
Click **OK**. The setting is remembered in **System Settings → Privacy & Security → Automation**.
|
|
73
|
+
|
|
74
|
+
`terminal_clear` additionally requires **Accessibility** permission (it uses System Events to simulate Cmd+K). Grant under **System Settings → Privacy & Security → Accessibility**.
|
|
75
|
+
|
|
76
|
+
## Scrollback configuration
|
|
77
|
+
|
|
78
|
+
For `terminal_read` to return meaningful history, set Terminal.app's scrollback to a generous size:
|
|
79
|
+
|
|
80
|
+
**Terminal.app → Settings → Profiles → (your profile) → Window → Scrollback**: set to **Unlimited** or a large fixed number.
|
|
81
|
+
|
|
82
|
+
## Register with Claude Code
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
claude mcp add macos-terminal --command=npx --args=-y --args=@priyanshumit/macos-terminal-mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or add to `~/.claude.json` / project `.mcp.json` manually:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"macos-terminal": {
|
|
94
|
+
"command": "npx",
|
|
95
|
+
"args": ["-y", "@priyanshumit/macos-terminal-mcp"]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
To enable write tools, add the env block:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"macos-terminal": {
|
|
107
|
+
"command": "npx",
|
|
108
|
+
"args": ["-y", "@priyanshumit/macos-terminal-mcp"],
|
|
109
|
+
"env": { "WRITE_TOOLS_ENABLED": "1" }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Restart Claude Code. You should see the eleven tools listed.
|
|
116
|
+
|
|
117
|
+
## Three-tier safety model
|
|
118
|
+
|
|
119
|
+
Every `terminal_execute` call is evaluated against a list of regex patterns, each tagged with a level:
|
|
120
|
+
|
|
121
|
+
| Level | Behavior |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `safe` | Auto-run, no confirmation |
|
|
124
|
+
| `requires_approval` | Native confirmation dialog (also enqueued for async approval via `pending_*`) |
|
|
125
|
+
| `forbidden` | **Refused outright** — no dialog can approve it. To run a forbidden command, do it yourself in a real terminal. |
|
|
126
|
+
|
|
127
|
+
**Evaluation rule: highest-restriction wins.** If a command matches both a `safe` pattern and a `forbidden` pattern, it's forbidden. This blocks composite-command bypasses like `ls && rm -rf /tmp/x` — the safe `^ls` match doesn't shield the forbidden `\brm\s+-rf?\b` match.
|
|
128
|
+
|
|
129
|
+
**Default if no pattern matches**: `requires_approval`. New/unknown commands always confirm.
|
|
130
|
+
|
|
131
|
+
## Default forbidden patterns
|
|
132
|
+
|
|
133
|
+
| Pattern | Why |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `\brm\s+-rf?\b` | Recursive delete |
|
|
136
|
+
| `\bsudo\b` | Privilege escalation — humans only |
|
|
137
|
+
| `\|\s*(bash\|sh\|zsh)\b` | Pipe-to-shell — common attack vector |
|
|
138
|
+
| `\bcurl\b[^\|;]*\|`, `\bwget\b[^\|;]*\|` | Curl/wget piped to anything |
|
|
139
|
+
| `>\s*/etc/`, `>\s*/dev/` | Writing to /etc or /dev |
|
|
140
|
+
| `/etc/passwd`, `/etc/shadow`, `~/.ssh` | System or credential files |
|
|
141
|
+
| `\bdd\s+if=` | dd — can overwrite disks |
|
|
142
|
+
| `\bgit\s+push\s+(--force\|-f)\b` | Force push |
|
|
143
|
+
| `\bgit\s+reset\s+--hard\b` | Discards local work |
|
|
144
|
+
| `\bgit\s+clean\s+-[fdx]+\b` | Destructive git clean |
|
|
145
|
+
| `\bshutdown\b`, `\breboot\b`, `\bkillall\b` | System control |
|
|
146
|
+
| `:\(\)\{:\|:&\};:` | Fork bomb |
|
|
147
|
+
|
|
148
|
+
The full list is in `src/safety/patterns.ts`. Customize via `safety_*` tools or by editing `~/.config/macos-terminal-mcp/safety.json` directly.
|
|
149
|
+
|
|
150
|
+
## Customizing patterns
|
|
151
|
+
|
|
152
|
+
**From within Claude:**
|
|
153
|
+
|
|
154
|
+
> *"Add `^cargo build` as a safe pattern."* → Claude calls `safety_add({pattern: "^cargo build", level: "safe"})` → you click Allow on the dialog → it's persisted.
|
|
155
|
+
|
|
156
|
+
> *"Show me the current safety policy."* → `safety_list` returns the JSON.
|
|
157
|
+
|
|
158
|
+
> *"Forbidden `^docker\\s+rm`."* → Claude calls `safety_add({pattern: "^docker\\s+rm", level: "forbidden"})` → dialog → persisted.
|
|
159
|
+
|
|
160
|
+
**By editing the file:**
|
|
161
|
+
|
|
162
|
+
`~/.config/macos-terminal-mcp/safety.json`:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"patterns": [
|
|
167
|
+
{ "pattern": "^cargo build\\b", "level": "safe", "description": "Rust builds" },
|
|
168
|
+
{ "pattern": "\\bmy-deploy-script\\b", "level": "forbidden", "description": "Never deploy from AI" }
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
If the file has the v1 schema (`{"allowlist": [...], "denylist": [...]}`), it's automatically migrated on load.
|
|
174
|
+
|
|
175
|
+
## Async approval queue
|
|
176
|
+
|
|
177
|
+
When `terminal_execute` triggers `requires_approval`, two things happen in parallel:
|
|
178
|
+
1. A native macOS dialog pops asking you to Allow/Deny.
|
|
179
|
+
2. The command is enqueued — visible via `pending_list`, resolvable via `pending_approve(id)` / `pending_deny(id, reason)`.
|
|
180
|
+
|
|
181
|
+
Whichever path resolves first wins. For solo desktop use, the dialog is the canonical signal. For headless/remote/team contexts, the queue tools provide an out-of-band approval path.
|
|
182
|
+
|
|
183
|
+
Pending entries auto-expire after 10 minutes. The queue is in-memory only — a server restart drops all pending entries.
|
|
184
|
+
|
|
185
|
+
## Usage examples
|
|
186
|
+
|
|
187
|
+
Once registered, from Claude Code:
|
|
188
|
+
|
|
189
|
+
- *"List my open terminals."* → `terminal_list`
|
|
190
|
+
- *"What's the last 50 lines from /dev/ttys062?"* → `terminal_read({tty: "/dev/ttys062", lines: 50})`
|
|
191
|
+
- *"Run `git status` in /dev/ttys054."* → `terminal_execute` (auto-runs, safe pattern)
|
|
192
|
+
- *"Run `rm -rf /tmp/cache` in /dev/ttys054."* → **refused** (matches forbidden pattern)
|
|
193
|
+
- *"Run `cargo test` in /dev/ttys054."* → dialog pops (no safe pattern matches, default requires_approval)
|
|
194
|
+
- *"Show me what's in the approval queue."* → `pending_list`
|
|
195
|
+
- *"Approve queued command abc-123."* → `pending_approve({id: "abc-123"})` — also pops a dialog
|
|
196
|
+
|
|
197
|
+
## Troubleshooting
|
|
198
|
+
|
|
199
|
+
**"osascript exited 1: ... not authorized"**
|
|
200
|
+
Automation permission has not been granted. Open System Settings → Privacy & Security → Automation, find the process, allow it to control Terminal.
|
|
201
|
+
|
|
202
|
+
**Confirmation dialogs never appear**
|
|
203
|
+
The MCP server is in a context without a GUI session. Test:
|
|
204
|
+
```bash
|
|
205
|
+
osascript -l JavaScript -e 'Application.currentApplication().includeStandardAdditions = true; Application.currentApplication().displayDialog("test")'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**`terminal_clear` does nothing**
|
|
209
|
+
Requires Accessibility permission, and the target window must accept keyboard focus. If you have multiple Terminal windows, focus may not settle on the intended one before the keystroke fires. Run `terminal_list` first to confirm the tty.
|
|
210
|
+
|
|
211
|
+
**`terminal_read` returns less than expected**
|
|
212
|
+
Terminal.app's scrollback cap is profile-controlled. Increase under Terminal.app → Settings → Profiles → Window → Scrollback.
|
|
213
|
+
|
|
214
|
+
**Command refused as "forbidden" but I have a legit reason**
|
|
215
|
+
Forbidden patterns intentionally cannot be approved via the tool. Either run the command yourself in a real terminal, or use `safety_set_level({pattern: "...", level: "requires_approval"})` to downgrade — you'll see a downgrade warning in the confirmation dialog.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export class OsascriptError extends Error {
|
|
3
|
+
stderr;
|
|
4
|
+
code;
|
|
5
|
+
timedOut;
|
|
6
|
+
constructor(message, stderr, code, timedOut = false) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.stderr = stderr;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.timedOut = timedOut;
|
|
11
|
+
this.name = "OsascriptError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
+
export function runJxa(script, options = {}) {
|
|
16
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const proc = spawn("osascript", ["-l", "JavaScript"], {
|
|
19
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
+
});
|
|
21
|
+
let stdout = "";
|
|
22
|
+
let stderr = "";
|
|
23
|
+
let settled = false;
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
if (settled)
|
|
26
|
+
return;
|
|
27
|
+
settled = true;
|
|
28
|
+
try {
|
|
29
|
+
proc.kill("SIGKILL");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
reject(new OsascriptError(`osascript timed out after ${timeoutMs}ms`, stderr.trim(), -1, true));
|
|
35
|
+
}, timeoutMs);
|
|
36
|
+
proc.stdout.on("data", (chunk) => {
|
|
37
|
+
stdout += chunk.toString("utf8");
|
|
38
|
+
});
|
|
39
|
+
proc.stderr.on("data", (chunk) => {
|
|
40
|
+
stderr += chunk.toString("utf8");
|
|
41
|
+
});
|
|
42
|
+
proc.on("error", (err) => {
|
|
43
|
+
if (settled)
|
|
44
|
+
return;
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
reject(err);
|
|
48
|
+
});
|
|
49
|
+
proc.on("close", (code) => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
settled = true;
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
if (code === 0) {
|
|
55
|
+
resolve(stdout.replace(/\n$/, ""));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
reject(new OsascriptError(`osascript exited ${code}: ${stderr.trim()}`, stderr.trim(), code ?? -1));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
proc.stdin.write(script);
|
|
62
|
+
proc.stdin.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export async function runJxaJson(script, options = {}) {
|
|
66
|
+
const output = await runJxa(script, options);
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(output);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const preview = output.length > 500 ? `${output.slice(0, 500)}…` : output;
|
|
72
|
+
throw new OsascriptError(`JXA stdout was not valid JSON: ${err.message}\nOutput: ${preview}`, output, -1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=applescript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applescript.js","sourceRoot":"","sources":["../src/applescript.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerAll } from "./tools/register.js";
|
|
5
|
+
const server = new McpServer({ name: "macos-terminal-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
6
|
+
registerAll(server);
|
|
7
|
+
async function main() {
|
|
8
|
+
const transport = new StdioServerTransport();
|
|
9
|
+
await server.connect(transport);
|
|
10
|
+
process.stderr.write("[macos-terminal-mcp] server ready on stdio\n");
|
|
11
|
+
}
|
|
12
|
+
main().catch((err) => {
|
|
13
|
+
process.stderr.write(`[macos-terminal-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
function resolveDefaultPath() {
|
|
5
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
6
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "state");
|
|
7
|
+
return join(base, "macos-terminal-mcp", "audit.log");
|
|
8
|
+
}
|
|
9
|
+
export const AUDIT_LOG_PATH = resolveDefaultPath();
|
|
10
|
+
const COMMAND_TRUNCATE = 1000;
|
|
11
|
+
function truncate(s, max) {
|
|
12
|
+
return s.length > max ? `${s.slice(0, max)}…[+${s.length - max}]` : s;
|
|
13
|
+
}
|
|
14
|
+
export async function appendAudit(entry, path = AUDIT_LOG_PATH) {
|
|
15
|
+
const record = {
|
|
16
|
+
timestamp: entry.timestamp ?? new Date().toISOString(),
|
|
17
|
+
...entry,
|
|
18
|
+
};
|
|
19
|
+
if (record.command !== undefined) {
|
|
20
|
+
record.command = truncate(record.command, COMMAND_TRUNCATE);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await mkdir(dirname(path), { recursive: true });
|
|
24
|
+
await appendFile(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
process.stderr.write(`[macos-terminal-mcp] audit log write failed: ${err.message}\n`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/safety/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACrD,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,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAA6D,EAC7D,OAAe,cAAc;IAE7B,MAAM,MAAM,GAAe;QACzB,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACtD,GAAG,KAAK;KACT,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,CAAC,CAAC;QAChD,MAAM,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gDAAiD,GAAa,CAAC,OAAO,IAAI,CAC3E,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { runJxa } from "../applescript.js";
|
|
2
|
+
export function isWriteToolsEnabled() {
|
|
3
|
+
return process.env.WRITE_TOOLS_ENABLED === "1";
|
|
4
|
+
}
|
|
5
|
+
const DEFAULT_DIALOG_TIMEOUT_SEC = 300;
|
|
6
|
+
export async function confirmWithUser(req) {
|
|
7
|
+
const allow = req.allowLabel ?? "Allow";
|
|
8
|
+
const deny = req.denyLabel ?? "Deny";
|
|
9
|
+
const timeoutSec = req.timeoutSeconds ?? DEFAULT_DIALOG_TIMEOUT_SEC;
|
|
10
|
+
const script = `
|
|
11
|
+
var app = Application.currentApplication();
|
|
12
|
+
app.includeStandardAdditions = true;
|
|
13
|
+
(function () {
|
|
14
|
+
try {
|
|
15
|
+
var result = app.displayDialog(
|
|
16
|
+
${JSON.stringify(req.message)},
|
|
17
|
+
{
|
|
18
|
+
buttons: [${JSON.stringify(deny)}, ${JSON.stringify(allow)}],
|
|
19
|
+
defaultButton: ${JSON.stringify(deny)},
|
|
20
|
+
cancelButton: ${JSON.stringify(deny)},
|
|
21
|
+
withTitle: ${JSON.stringify(req.title)},
|
|
22
|
+
withIcon: "caution",
|
|
23
|
+
givingUpAfter: ${timeoutSec}
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
if (result.gaveUp) return "TIMEOUT";
|
|
27
|
+
return result.buttonReturned === ${JSON.stringify(allow)} ? "ALLOW" : "DENY";
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return "DENY";
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
`;
|
|
33
|
+
// Outer osascript timeout is dialog timeout + 10s buffer so the dialog's own
|
|
34
|
+
// givingUpAfter has a chance to fire before the spawn-level kill engages.
|
|
35
|
+
const result = await runJxa(script, { timeoutMs: (timeoutSec + 10) * 1000 });
|
|
36
|
+
return result.trim() === "ALLOW";
|
|
37
|
+
}
|
|
38
|
+
export function writeToolsDisabledMessage(toolName) {
|
|
39
|
+
return (`${toolName} is disabled. Write tools (terminal_execute, terminal_clear, safety_*) ` +
|
|
40
|
+
`are off by default for safety. To enable, set environment variable ` +
|
|
41
|
+
`WRITE_TOOLS_ENABLED=1 in the MCP server's env block. Each non-"safe" call ` +
|
|
42
|
+
`still prompts via a native macOS dialog.`);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=confirm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/safety/confirm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,UAAU,mBAAmB;IACjC,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,KAAK,GAAG,CAAC;AACjD,CAAC;AAWD,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAmB;IACvD,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC;IACxC,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,IAAI,MAAM,CAAC;IACrC,MAAM,UAAU,GAAG,GAAG,CAAC,cAAc,IAAI,0BAA0B,CAAC;IACpE,MAAM,MAAM,GAAG;;;;;;QAMT,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;;oBAEf,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;yBACzC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;wBACrB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;qBACvB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;;yBAErB,UAAU;;;;uCAII,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;;;CAK3D,CAAC;IACA,6EAA6E;IAC7E,0EAA0E;IAC1E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAC7E,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,QAAgB;IACxD,OAAO,CACL,GAAG,QAAQ,yEAAyE;QACpF,qEAAqE;QACrE,4EAA4E;QAC5E,0CAA0C,CAC3C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export const SAFETY_CONFIG_PATH = join(homedir(), ".config", "macos-terminal-mcp", "safety.json");
|
|
5
|
+
const DEFAULT_PATTERNS = [
|
|
6
|
+
// FORBIDDEN — never run, even with confirmation. The user must run these in a terminal themselves.
|
|
7
|
+
{
|
|
8
|
+
pattern: "\\brm\\s+-rf?\\b",
|
|
9
|
+
level: "forbidden",
|
|
10
|
+
description: "Recursive rm — too destructive to expose to an AI agent",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: "\\bsudo\\b",
|
|
14
|
+
level: "forbidden",
|
|
15
|
+
description: "Privilege escalation should always be done by a human",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
pattern: "\\|\\s*(bash|sh|zsh)\\b",
|
|
19
|
+
level: "forbidden",
|
|
20
|
+
description: "Piping into a shell — common attack vector",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: "\\bcurl\\b[^|;]*\\|",
|
|
24
|
+
level: "forbidden",
|
|
25
|
+
description: "curl piped to another command",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
pattern: "\\bwget\\b[^|;]*\\|",
|
|
29
|
+
level: "forbidden",
|
|
30
|
+
description: "wget piped to another command",
|
|
31
|
+
},
|
|
32
|
+
{ pattern: ">\\s*/etc/", level: "forbidden", description: "Writing to /etc" },
|
|
33
|
+
{ pattern: ">\\s*/dev/", level: "forbidden", description: "Writing to /dev" },
|
|
34
|
+
{ pattern: "/etc/passwd", level: "forbidden", description: "Touching /etc/passwd" },
|
|
35
|
+
{ pattern: "/etc/shadow", level: "forbidden", description: "Touching /etc/shadow" },
|
|
36
|
+
{ pattern: "~/.ssh", level: "forbidden", description: "Touching SSH keys" },
|
|
37
|
+
{ pattern: "\\bdd\\s+if=", level: "forbidden", description: "dd — can overwrite disks" },
|
|
38
|
+
{ pattern: ":\\(\\)\\{:\\|:&\\};:", level: "forbidden", description: "Fork bomb" },
|
|
39
|
+
{ pattern: "\\bshutdown\\b", level: "forbidden", description: "System shutdown" },
|
|
40
|
+
{ pattern: "\\breboot\\b", level: "forbidden", description: "System reboot" },
|
|
41
|
+
{ pattern: "\\bkillall\\b", level: "forbidden", description: "Mass process kill" },
|
|
42
|
+
{
|
|
43
|
+
pattern: "\\bgit\\s+push\\s+(--force|-f)\\b",
|
|
44
|
+
level: "forbidden",
|
|
45
|
+
description: "Force push — usually a mistake",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: "\\bgit\\s+reset\\s+--hard\\b",
|
|
49
|
+
level: "forbidden",
|
|
50
|
+
description: "git reset --hard — discards local work",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
pattern: "\\bgit\\s+clean\\s+-[fdx]+\\b",
|
|
54
|
+
level: "forbidden",
|
|
55
|
+
description: "git clean with -f flags — destructive",
|
|
56
|
+
},
|
|
57
|
+
// SAFE — auto-run, no confirmation needed.
|
|
58
|
+
{ pattern: "^ls(\\s|$)", level: "safe", description: "List directory contents" },
|
|
59
|
+
{ pattern: "^pwd(\\s|$)", level: "safe", description: "Print working directory" },
|
|
60
|
+
{ pattern: "^cd(\\s|$)", level: "safe", description: "Change directory" },
|
|
61
|
+
{ pattern: "^echo(\\s|$)", level: "safe", description: "Echo arguments" },
|
|
62
|
+
{ pattern: "^cat\\s", level: "safe", description: "Print file contents" },
|
|
63
|
+
{ pattern: "^less\\s", level: "safe", description: "Page through a file" },
|
|
64
|
+
{ pattern: "^head\\s", level: "safe", description: "First N lines of a file" },
|
|
65
|
+
{ pattern: "^tail\\s", level: "safe", description: "Last N lines of a file" },
|
|
66
|
+
{ pattern: "^which\\s", level: "safe", description: "Locate a command" },
|
|
67
|
+
{ pattern: "^type\\s", level: "safe", description: "Describe a command" },
|
|
68
|
+
{ pattern: "^date(\\s|$)", level: "safe", description: "Current date/time" },
|
|
69
|
+
{ pattern: "^whoami(\\s|$)", level: "safe", description: "Current user" },
|
|
70
|
+
{ pattern: "^uptime(\\s|$)", level: "safe", description: "System uptime" },
|
|
71
|
+
{
|
|
72
|
+
pattern: "^git\\s+(status|log|diff|branch|show|remote|stash list)\\b",
|
|
73
|
+
level: "safe",
|
|
74
|
+
description: "Read-only git operations",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
pattern: "^npm\\s+(test|run\\s+test|run\\s+lint|run\\s+typecheck)\\b",
|
|
78
|
+
level: "safe",
|
|
79
|
+
description: "Common npm read-ish operations",
|
|
80
|
+
},
|
|
81
|
+
{ pattern: "^node\\s+--version\\b", level: "safe" },
|
|
82
|
+
{ pattern: "^python3?\\s+--version\\b", level: "safe" },
|
|
83
|
+
];
|
|
84
|
+
export async function loadSafetyConfig(path = SAFETY_CONFIG_PATH) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = await readFile(path, "utf8");
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
return normalizeConfig(parsed);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { patterns: DEFAULT_PATTERNS };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function saveSafetyConfig(config, path = SAFETY_CONFIG_PATH) {
|
|
95
|
+
await mkdir(dirname(path), { recursive: true });
|
|
96
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
97
|
+
}
|
|
98
|
+
export function defaultPatterns() {
|
|
99
|
+
return DEFAULT_PATTERNS.map((p) => ({ ...p }));
|
|
100
|
+
}
|
|
101
|
+
export function normalizeConfig(raw) {
|
|
102
|
+
if (!raw || typeof raw !== "object")
|
|
103
|
+
return { patterns: DEFAULT_PATTERNS };
|
|
104
|
+
const o = raw;
|
|
105
|
+
if (Array.isArray(o.patterns)) {
|
|
106
|
+
return {
|
|
107
|
+
patterns: o.patterns.filter((e) => isValidEntry(e)),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Migrate v1 schema {allowlist: [], denylist: []} → v2 {patterns: [...]}
|
|
111
|
+
if (Array.isArray(o.allowlist) || Array.isArray(o.denylist)) {
|
|
112
|
+
const patterns = [];
|
|
113
|
+
if (Array.isArray(o.denylist)) {
|
|
114
|
+
for (const p of o.denylist) {
|
|
115
|
+
if (typeof p === "string") {
|
|
116
|
+
patterns.push({
|
|
117
|
+
pattern: p,
|
|
118
|
+
level: "requires_approval",
|
|
119
|
+
description: "Migrated from v1 denylist",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(o.allowlist)) {
|
|
125
|
+
for (const p of o.allowlist) {
|
|
126
|
+
if (typeof p === "string") {
|
|
127
|
+
patterns.push({
|
|
128
|
+
pattern: p,
|
|
129
|
+
level: "safe",
|
|
130
|
+
description: "Migrated from v1 allowlist",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { patterns: patterns.length > 0 ? patterns : DEFAULT_PATTERNS };
|
|
136
|
+
}
|
|
137
|
+
return { patterns: DEFAULT_PATTERNS };
|
|
138
|
+
}
|
|
139
|
+
function isValidEntry(e) {
|
|
140
|
+
if (!e || typeof e !== "object")
|
|
141
|
+
return false;
|
|
142
|
+
const o = e;
|
|
143
|
+
return (typeof o.pattern === "string" &&
|
|
144
|
+
(o.level === "safe" || o.level === "requires_approval" || o.level === "forbidden") &&
|
|
145
|
+
(o.description === undefined || typeof o.description === "string"));
|
|
146
|
+
}
|
|
147
|
+
const LEVEL_RANK = {
|
|
148
|
+
safe: 0,
|
|
149
|
+
requires_approval: 1,
|
|
150
|
+
forbidden: 2,
|
|
151
|
+
};
|
|
152
|
+
export function evaluateCommand(command, config) {
|
|
153
|
+
let result = null;
|
|
154
|
+
for (const entry of config.patterns) {
|
|
155
|
+
if (!testPattern(entry.pattern, command))
|
|
156
|
+
continue;
|
|
157
|
+
if (!result || LEVEL_RANK[entry.level] > LEVEL_RANK[result.level]) {
|
|
158
|
+
result = {
|
|
159
|
+
level: entry.level,
|
|
160
|
+
matchedPattern: entry.pattern,
|
|
161
|
+
matchedDescription: entry.description,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// No pattern matched — default is requires_approval (safer than safe)
|
|
166
|
+
return result ?? { level: "requires_approval" };
|
|
167
|
+
}
|
|
168
|
+
function testPattern(pattern, command) {
|
|
169
|
+
try {
|
|
170
|
+
return new RegExp(pattern).test(command);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/safety/patterns.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAc1C,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,oBAAoB,EAAE,aAAa,CAAC,CAAC;AAElG,MAAM,gBAAgB,GAAmB;IACvC,mGAAmG;IACnG;QACE,OAAO,EAAE,kBAAkB;QAC3B,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,yDAAyD;KACvE;IACD;QACE,OAAO,EAAE,YAAY;QACrB,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,uDAAuD;KACrE;IACD;QACE,OAAO,EAAE,yBAAyB;QAClC,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,4CAA4C;KAC1D;IACD;QACE,OAAO,EAAE,qBAAqB;QAC9B,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,+BAA+B;KAC7C;IACD;QACE,OAAO,EAAE,qBAAqB;QAC9B,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,+BAA+B;KAC7C;IACD,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE;IAC7E,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE;IAC7E,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,sBAAsB,EAAE;IACnF,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,sBAAsB,EAAE;IACnF,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE;IAC3E,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACxF,EAAE,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE;IAClF,EAAE,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE;IACjF,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE;IAC7E,EAAE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE;IAClF;QACE,OAAO,EAAE,mCAAmC;QAC5C,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,gCAAgC;KAC9C;IACD;QACE,OAAO,EAAE,8BAA8B;QACvC,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,wCAAwC;KACtD;IACD;QACE,OAAO,EAAE,+BAA+B;QACxC,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,uCAAuC;KACrD;IAED,2CAA2C;IAC3C,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,yBAAyB,EAAE;IAChF,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,yBAAyB,EAAE;IACjF,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,EAAE;IACzE,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,gBAAgB,EAAE;IACzE,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACzE,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,qBAAqB,EAAE;IAC1E,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,yBAAyB,EAAE;IAC9E,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC7E,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,EAAE;IACxE,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACzE,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,mBAAmB,EAAE;IAC5E,EAAE,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE;IACzE,EAAE,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,eAAe,EAAE;IAC1E;QACE,OAAO,EAAE,4DAA4D;QACrE,KAAK,EAAE,MAAM;QACb,WAAW,EAAE,0BAA0B;KACxC;IACD;QACE,OAAO,EAAE,4DAA4D;QACrE,KAAK,EAAE,MAAM;QACb,WAAW,EAAE,gCAAgC;KAC9C;IACD,EAAE,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,EAAE;IACnD,EAAE,OAAO,EAAE,2BAA2B,EAAE,KAAK,EAAE,MAAM,EAAE;CACxD,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAe,kBAAkB;IACtE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IACxC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAoB,EACpB,OAAe,kBAAkB;IAEjC,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC3E,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAU,EAAqB,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;SAChF,CAAC;IACJ,CAAC;IACD,yEAAyE;IACzE,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,QAAQ,GAAmB,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;oBAC1B,QAAQ,CAAC,IAAI,CAAC;wBACZ,OAAO,EAAE,CAAC;wBACV,KAAK,EAAE,mBAAmB;wBAC1B,WAAW,EAAE,2BAA2B;qBACzC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;gBAC5B,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;oBAC1B,QAAQ,CAAC,IAAI,CAAC;wBACZ,OAAO,EAAE,CAAC;wBACV,KAAK,EAAE,MAAM;wBACb,WAAW,EAAE,4BAA4B;qBAC1C,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,YAAY,CAAC,CAAU;IAC9B,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,CAAC,GAAG,CAA4B,CAAC;IACvC,OAAO,CACL,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAC7B,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,KAAK,KAAK,mBAAmB,IAAI,CAAC,CAAC,KAAK,KAAK,WAAW,CAAC;QAClF,CAAC,CAAC,CAAC,WAAW,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,CAAC,CACnE,CAAC;AACJ,CAAC;AAQD,MAAM,UAAU,GAAgC;IAC9C,IAAI,EAAE,CAAC;IACP,iBAAiB,EAAE,CAAC;IACpB,SAAS,EAAE,CAAC;CACb,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,MAAoB;IACnE,IAAI,MAAM,GAAyB,IAAI,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC;YAAE,SAAS;QACnD,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAClE,MAAM,GAAG;gBACP,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,cAAc,EAAE,KAAK,CAAC,OAAO;gBAC7B,kBAAkB,EAAE,KAAK,CAAC,WAAW;aACtC,CAAC;QACJ,CAAC;IACH,CAAC;IACD,sEAAsE;IACtE,OAAO,MAAM,IAAI,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;AAClD,CAAC;AAED,SAAS,WAAW,CAAC,OAAe,EAAE,OAAe;IACnD,IAAI,CAAC;QACH,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|