@mindfullabai/onda-mcp 0.1.0 → 0.3.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 +57 -4
- package/bin/install-skill.mjs +21 -0
- package/dist/index.js +324 -4
- package/package.json +12 -5
- package/scripts/install-skill.mjs +115 -0
- package/skills/onda-mcp-usage/SKILL.md +326 -0
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server to control [Onda](https://onda.dev) terminal from AI agents (Claude Code, Cursor, Windsurf, etc.)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
31 tools across 7 categories let AI agents split panes, run commands, manage tabs and workspaces, coordinate across multiple windows, and orchestrate multi-agent workflows -- all through the standard [Model Context Protocol](https://modelcontextprotocol.io/).
|
|
6
|
+
|
|
7
|
+
Since v0.2.0 Onda MCP is **multi-window aware**: agents can discover windows, locate workspaces, address terminals unambiguously (each entry carries `windowId` + `workspaceId` + `paneId`), and launch full Claude/agent sessions in one atomic call with the `onda_launch_session` macro.
|
|
6
8
|
|
|
7
9
|
## Requirements
|
|
8
10
|
|
|
@@ -62,7 +64,8 @@ Same MCP config format as above.
|
|
|
62
64
|
|------|-------------|
|
|
63
65
|
| `onda_terminal_run` | Run a command in a specific terminal (sends command + newline). |
|
|
64
66
|
| `onda_terminal_send` | Send raw text to a terminal without appending a newline. Use for interactive input or key sequences. |
|
|
65
|
-
| `onda_terminal_list` | List
|
|
67
|
+
| `onda_terminal_list` | List terminals with `{id, pid, cwd, alive, workspaceId, paneId, tabId, windowId}`. Optional input filters `workspaceId` / `windowId`. |
|
|
68
|
+
| `onda_terminal_spawn` | Spawn a binary inside an existing pane via `exec bin args...`, preserving multi-line/quoted argv. Use to start `claude` (or any agent) with a structured prompt in a pre-existing workspace pane. |
|
|
66
69
|
| `onda_terminal_kill` | Kill a terminal process by ID. |
|
|
67
70
|
|
|
68
71
|
### Tab Management
|
|
@@ -78,11 +81,28 @@ Same MCP config format as above.
|
|
|
78
81
|
|
|
79
82
|
| Tool | Description |
|
|
80
83
|
|------|-------------|
|
|
81
|
-
| `onda_workspace_list` | List
|
|
84
|
+
| `onda_workspace_list` | List workspaces with `{id, name, rootPath, mountedIn}` (mountedIn = windowId currently hosting that workspace, or null). |
|
|
82
85
|
| `onda_workspace_create` | Create a new workspace. Workspaces group tabs by project. |
|
|
83
86
|
| `onda_workspace_focus` | Switch to a workspace by ID. |
|
|
84
|
-
| `onda_workspace_add_terminal` | Add a new terminal pane to a workspace (tiled mode). |
|
|
87
|
+
| `onda_workspace_add_terminal` | Add a new terminal pane to a workspace (tiled mode). Returns `{terminalId, paneId, workspaceId, windowId, ready}`. PTY-ready handshake is on by default — set `waitForReady:false` to skip. |
|
|
85
88
|
| `onda_workspace_tile` | Set the workspace tiling layout: `single`, `split-h`, `split-v`, or `quad`. |
|
|
89
|
+
| `onda_workspace_locate` | Resolve a workspace by `id` / `name` / `rootPath` without listing them all. Returns `{workspace: {id, name, rootPath, mountedIn} | null}`. |
|
|
90
|
+
|
|
91
|
+
### Window (multi-window)
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `onda_window_list` | List Onda main windows. Each entry: `{windowId, isFocused, title, workspaceIds[], activeWorkspaceId, uiMode}`. |
|
|
96
|
+
| `onda_window_new` | Open a fresh empty main window. Returns `{windowId}`. |
|
|
97
|
+
| `onda_window_focus` | Bring a window to the foreground (restore + raise + focus). When `windowId` is omitted, focuses the primary window. |
|
|
98
|
+
| `onda_window_mount_workspace` | Mount a workspace in a specific window. Idempotent; orchestrates an atomic transfer if the workspace is currently owned by another window. Returns `{success, workspaceId, windowId, transferred}`. |
|
|
99
|
+
| `onda_workspace_unmount` | Remove a workspace from whichever window currently hosts it. The workspace continues to exist globally — only its on-screen mounting is dropped. Returns `{success, workspaceId, windowId, alreadyUnmounted}`. |
|
|
100
|
+
|
|
101
|
+
### Macro
|
|
102
|
+
|
|
103
|
+
| Tool | Description |
|
|
104
|
+
|------|-------------|
|
|
105
|
+
| `onda_launch_session` | Atomic "ensure workspace + mount in target window + add terminal + spawn `bin` with `args`/`prompt`". Supports `placement: 'auto'` / `'current-window'` / `'window:<id>'` / `'new-window'` / `'ask-user'`. When `ask-user`, returns `{needsDecision: true, options, workspace}` so the host agent can surface the choice to the user, then re-invokes the tool with a concrete `placement`. |
|
|
86
106
|
|
|
87
107
|
### System
|
|
88
108
|
|
|
@@ -103,6 +123,39 @@ Same MCP config format as above.
|
|
|
103
123
|
| `ONDA_WORKSPACE_ID` | Workspace ID | auto-detected |
|
|
104
124
|
| `ONDA_TERMINAL` | Set to `"1"` when inside Onda | auto-detected |
|
|
105
125
|
|
|
126
|
+
## Agent-bus pattern (since v0.2.0)
|
|
127
|
+
|
|
128
|
+
Typical flow when an agent in window A delegates a task to another agent in workspace X (which may or may not be already mounted somewhere):
|
|
129
|
+
|
|
130
|
+
```jsonc
|
|
131
|
+
// 1. Try a fully-automatic launch
|
|
132
|
+
onda_launch_session({
|
|
133
|
+
workspace: { name: "brandart-agentic-platform" },
|
|
134
|
+
bin: "claude",
|
|
135
|
+
prompt: "Scaffold modulo memoria — segui il brief #BAP-2026-05-20 ...",
|
|
136
|
+
placement: "ask-user"
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// → If multiple windows exist, the tool returns without acting:
|
|
140
|
+
{
|
|
141
|
+
"needsDecision": true,
|
|
142
|
+
"reason": "placement: ask-user",
|
|
143
|
+
"options": [
|
|
144
|
+
{ "placement": "window:w-abc12345", "label": "Window w-abc1234 (current, focused)" },
|
|
145
|
+
{ "placement": "window:w-def67890", "label": "Window w-def6789" },
|
|
146
|
+
{ "placement": "new-window", "label": "Open in a new window" }
|
|
147
|
+
],
|
|
148
|
+
"workspace": { "id": "ws-...", "name": "brandart-agentic-platform", "rootPath": "/Users/mario/Projects/06-Brandart/brandart-agentic-platform" }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. Host agent shows options to the human, gets choice, re-invokes:
|
|
152
|
+
onda_launch_session({ /* same args */, placement: "window:w-abc12345" })
|
|
153
|
+
// → { windowId, workspaceId, terminalId, paneId, pid }
|
|
154
|
+
// Claude is now running in that pane with the brief as its initial prompt.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
After launch, the caller can keep talking to the spawned agent via `onda_terminal_send` / `onda_terminal_run` using the returned `terminalId`, or use `onda_terminal_list` (now enriched with `workspaceId`/`windowId`) to disambiguate which terminal belongs to which session.
|
|
158
|
+
|
|
106
159
|
## Architecture
|
|
107
160
|
|
|
108
161
|
- Communicates with Onda via **Unix domain socket**
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `onda-mcp-install-skill` bin entry. Forwards to scripts/install-skill.mjs.
|
|
4
|
+
*
|
|
5
|
+
* Distributed via package.json `bin` so users can run:
|
|
6
|
+
* npx @mindfullabai/onda-mcp install-skill
|
|
7
|
+
*
|
|
8
|
+
* (npm executes the matching `onda-mcp-install-skill` binary; we ship a
|
|
9
|
+
* minimal wrapper to allow forwarding any argv.)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const target = resolve(__dirname, '..', 'scripts', 'install-skill.mjs');
|
|
18
|
+
const result = spawnSync(process.execPath, [target, ...process.argv.slice(2)], {
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
});
|
|
21
|
+
process.exit(result.status ?? 0);
|
package/dist/index.js
CHANGED
|
@@ -176,10 +176,13 @@ const TOOLS = [
|
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
name: 'onda_terminal_list',
|
|
179
|
-
description: 'List
|
|
179
|
+
description: 'List active terminals. Each entry: { id, pid, cwd, alive, workspaceId, paneId, tabId, windowId }. Use the optional filters to scope the list. Essential to disambiguate which terminal belongs to which workspace/window when many are alive — no more cwd reverse-lookup.',
|
|
180
180
|
inputSchema: {
|
|
181
181
|
type: 'object',
|
|
182
|
-
properties: {
|
|
182
|
+
properties: {
|
|
183
|
+
workspaceId: { type: 'string', description: 'Filter: only terminals in this workspace.' },
|
|
184
|
+
windowId: { type: 'string', description: 'Filter: only terminals in this window.' },
|
|
185
|
+
},
|
|
183
186
|
},
|
|
184
187
|
},
|
|
185
188
|
{
|
|
@@ -193,6 +196,144 @@ const TOOLS = [
|
|
|
193
196
|
required: ['id'],
|
|
194
197
|
},
|
|
195
198
|
},
|
|
199
|
+
// --- Terminal Tap (read + subscribe + sendKeys + waitFor) ---
|
|
200
|
+
{
|
|
201
|
+
name: 'onda_terminal_read',
|
|
202
|
+
description: 'Read recent output of a terminal (ring buffer ~200 KB / ~1 MB if a listener is attached). Returns the current buffer content plus byte total and timestamps. Use to see what a terminal has printed without subscribing to a live stream. Lazily attaches a passive tap on first call — no impact on the terminal itself.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
id: { type: 'string', description: 'Terminal ID to read from.' },
|
|
207
|
+
lines: {
|
|
208
|
+
type: 'number',
|
|
209
|
+
description: 'Optional: cap returned content to the last N lines.',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
required: ['id'],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'onda_terminal_subscribe',
|
|
217
|
+
description: 'Attach a long-lived listener to a terminal\'s output stream. Returns a sessionId for use with onda_terminal_poll. Also returns the current buffer snapshot so the listener has full context. Cap of 4 concurrent listeners per terminal. The listener\'s name is shown in the Onda UI as a presence indicator.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
id: { type: 'string', description: 'Terminal ID to subscribe to.' },
|
|
222
|
+
listener: {
|
|
223
|
+
type: 'string',
|
|
224
|
+
description: 'Display name for this listener (shown in Onda UI presence badge). e.g. "alita", "kai", "ci-watch".',
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ['id', 'listener'],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'onda_terminal_poll',
|
|
232
|
+
description: 'Long-poll a subscribed terminal session for new output. Blocks up to timeoutMs (default 15000) waiting for new chunks. Returns immediately if data is already pending. Each call advances the cursor; only data emitted after the last poll is returned. Use in a loop: subscribe -> poll -> poll -> ... -> unsubscribe.',
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
sessionId: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'Session ID returned by onda_terminal_subscribe.',
|
|
239
|
+
},
|
|
240
|
+
timeoutMs: {
|
|
241
|
+
type: 'number',
|
|
242
|
+
description: 'Max wait in ms before returning empty. Default 15000.',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
required: ['sessionId'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'onda_terminal_unsubscribe',
|
|
250
|
+
description: 'Detach a listener session. Idempotent. Always call this when done to free the ring buffer (drops back to idle size after the last subscriber leaves).',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
sessionId: { type: 'string', description: 'Session ID to detach.' },
|
|
255
|
+
},
|
|
256
|
+
required: ['sessionId'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'onda_terminal_listeners',
|
|
261
|
+
description: 'List currently attached listeners for a terminal. Returns name + sessionId + timestamps. Used to inspect who is observing a terminal (introspection / debugging).',
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
id: { type: 'string', description: 'Terminal ID to inspect.' },
|
|
266
|
+
},
|
|
267
|
+
required: ['id'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'onda_terminal_wait_for',
|
|
272
|
+
description: 'Block until a regex pattern matches new terminal output, or timeout. Useful to synchronize scripted command sequences: run command -> wait for prompt -> run next command. Pattern is a JavaScript regex string; flags default to "m" (multiline). Returns { matched: bool, match?: string }.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
id: { type: 'string', description: 'Terminal ID to watch.' },
|
|
277
|
+
pattern: { type: 'string', description: 'Regex pattern (string form).' },
|
|
278
|
+
flags: { type: 'string', description: 'Regex flags. Default "m".' },
|
|
279
|
+
timeoutMs: { type: 'number', description: 'Max wait in ms. Default 30000.' },
|
|
280
|
+
},
|
|
281
|
+
required: ['id', 'pattern'],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
// --- Spatial awareness (M+1 follow-up) ---
|
|
285
|
+
{
|
|
286
|
+
name: 'onda_workspace_layout',
|
|
287
|
+
description: 'Read the current layout of a workspace: mosaic tree, list of pane IDs, active pane, viewport dimensions, and per-pane cwd. Use this BEFORE spawning a new terminal to decide where to place it (right/down/replace) and whether the viewport has room. Returns null for `workspace` if the id is unknown. Omit workspaceId to use the active workspace.',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
workspaceId: { type: 'string', description: 'Workspace ID. Omit for active workspace.' },
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: 'onda_window_screenshot',
|
|
297
|
+
description: 'Capture a PNG/JPEG snapshot of an Onda main window for visual debugging. Returns either a base64 dataUrl or a tempfile path. Use this when you (the agent) need to SEE what the user sees — verifying a layout change, confirming a feature renders correctly, or investigating UI glitches. Returns { dataUrl | path, width, height, windowId, capturedAt }. Defaults: focused window, PNG format, dataUrl=true.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
windowId: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'Window ID to capture. Omit for the focused window.',
|
|
304
|
+
},
|
|
305
|
+
format: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
enum: ['png', 'jpeg'],
|
|
308
|
+
description: 'Output format. PNG is lossless (default), JPEG is smaller.',
|
|
309
|
+
},
|
|
310
|
+
quality: {
|
|
311
|
+
type: 'number',
|
|
312
|
+
description: 'JPEG quality 0..100 (default 80). Ignored for PNG.',
|
|
313
|
+
},
|
|
314
|
+
dataUrl: {
|
|
315
|
+
type: 'boolean',
|
|
316
|
+
description: 'When true (default), returns base64 dataUrl. When false, writes a tempfile and returns its path — useful for large captures.',
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'onda_terminal_send_keys',
|
|
323
|
+
description: 'Send semantic key sequences to a terminal (Ctrl+C, Up, Enter, Esc, F5, Tab, ...). Each entry in `keys` is mapped to the appropriate stdin bytes. Supports Ctrl+<letter>, Arrow keys, Function keys F1-F12, Enter/Tab/Esc/Backspace/Delete/Home/End/PageUp/PageDown/Insert/Space, and raw literal text as fallback. Use this for tmux-like control instead of onda_terminal_send when you need to type special keys.',
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {
|
|
327
|
+
id: { type: 'string', description: 'Terminal ID to send to.' },
|
|
328
|
+
keys: {
|
|
329
|
+
type: 'array',
|
|
330
|
+
items: { type: 'string' },
|
|
331
|
+
description: 'Ordered list of key names. Example: ["Ctrl+C"], ["Up", "Up", "Enter"], ["Esc", ":wq", "Enter"].',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
required: ['id', 'keys'],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
196
337
|
// --- Tab ---
|
|
197
338
|
{
|
|
198
339
|
name: 'onda_tab_new',
|
|
@@ -234,10 +375,28 @@ const TOOLS = [
|
|
|
234
375
|
required: ['id'],
|
|
235
376
|
},
|
|
236
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: 'onda_tab_exec',
|
|
380
|
+
description: 'Open a new tab and spawn a process directly with exact argv (bypasses shell parsing). Use this when you need to pass multi-line strings or special characters as a single argument — e.g. launching `claude` with a structured preamble. The process replaces the shell in the tab\'s PTY: argv is passed to execve() as-is, so embedded newlines, quotes, and dollar signs are preserved verbatim.',
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
bin: { type: 'string', description: 'Absolute path or PATH-resolvable binary to spawn (e.g., "claude", "/usr/local/bin/aider").' },
|
|
385
|
+
args: {
|
|
386
|
+
type: 'array',
|
|
387
|
+
items: { type: 'string' },
|
|
388
|
+
description: 'Arguments passed verbatim to the binary. Each element is one argv entry; no shell parsing happens.',
|
|
389
|
+
},
|
|
390
|
+
cwd: { type: 'string', description: 'Working directory for the spawned process.' },
|
|
391
|
+
workspaceId: { type: 'string', description: 'Workspace ID to host the new tab. Omit to use active workspace.' },
|
|
392
|
+
},
|
|
393
|
+
required: ['bin'],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
237
396
|
// --- Workspace ---
|
|
238
397
|
{
|
|
239
398
|
name: 'onda_workspace_list',
|
|
240
|
-
description: 'List all workspaces
|
|
399
|
+
description: 'List all workspaces. Each entry: { id, name, rootPath, mountedIn }. `mountedIn` is the windowId hosting that workspace, or null if not currently mounted. Use this with onda_window_list to map workspaces ↔ windows.',
|
|
241
400
|
inputSchema: {
|
|
242
401
|
type: 'object',
|
|
243
402
|
properties: {},
|
|
@@ -269,13 +428,23 @@ const TOOLS = [
|
|
|
269
428
|
},
|
|
270
429
|
{
|
|
271
430
|
name: 'onda_workspace_add_terminal',
|
|
272
|
-
description: 'Add a new terminal pane to a workspace.
|
|
431
|
+
description: 'Add a new terminal pane to a workspace and wait for its PTY to be ready. Returns { success, terminalId, paneId, workspaceId, windowId, ready }. \n\nPlacement: by default the new pane is appended via react-mosaic\'s built-in logic (typically "split right"). For deterministic placement, pass `direction` and (optionally) `relativeToPaneId` — this routes through splitPane and gives you explicit control.\n\nCALL onda_workspace_layout FIRST when you care about placement: it returns the current mosaic tree + active pane + viewport, so you can decide whether to stack "down" (output-heavy panes) or split "right" (parallel-watch panes).',
|
|
273
432
|
inputSchema: {
|
|
274
433
|
type: 'object',
|
|
275
434
|
properties: {
|
|
276
435
|
workspaceId: { type: 'string', description: 'Workspace ID. Omit to use active workspace.' },
|
|
277
436
|
cwd: { type: 'string', description: 'Working directory for the new terminal.' },
|
|
278
437
|
shell: { type: 'string', description: 'Shell to use (e.g., /bin/zsh).' },
|
|
438
|
+
waitForReady: { type: 'boolean', description: 'Default true. When false, returns as soon as the pane object is created (PTY may still be spawning).' },
|
|
439
|
+
direction: {
|
|
440
|
+
type: 'string',
|
|
441
|
+
enum: ['right', 'down', 'up', 'left', 'horizontal', 'vertical'],
|
|
442
|
+
description: 'Optional. When set, the pane is created by splitting an existing one in this direction. "horizontal"/"down"/"up" produce a top/bottom pair; "vertical"/"right"/"left" produce a left/right pair.',
|
|
443
|
+
},
|
|
444
|
+
relativeToPaneId: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
description: 'Optional pane ID to split. When omitted with `direction` set, the active pane is used.',
|
|
447
|
+
},
|
|
279
448
|
},
|
|
280
449
|
},
|
|
281
450
|
},
|
|
@@ -295,6 +464,119 @@ const TOOLS = [
|
|
|
295
464
|
required: ['layout'],
|
|
296
465
|
},
|
|
297
466
|
},
|
|
467
|
+
{
|
|
468
|
+
name: 'onda_workspace_locate',
|
|
469
|
+
description: 'Find a workspace by name, id, or rootPath without listing them all. Returns { workspace: { id, name, rootPath, mountedIn } } or { workspace: null }. mountedIn is the windowId currently hosting the workspace, or null if not mounted.',
|
|
470
|
+
inputSchema: {
|
|
471
|
+
type: 'object',
|
|
472
|
+
properties: {
|
|
473
|
+
id: { type: 'string', description: 'Workspace ID to look up.' },
|
|
474
|
+
name: { type: 'string', description: 'Workspace name to look up.' },
|
|
475
|
+
rootPath: { type: 'string', description: 'Workspace rootPath to look up.' },
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
// --- Window (multi-window aware) ---
|
|
480
|
+
{
|
|
481
|
+
name: 'onda_window_list',
|
|
482
|
+
description: 'List all Onda main windows. Returns { windows: [{ windowId, isFocused, title, workspaceIds[], activeWorkspaceId, uiMode }] }. Use this before placing a new workspace/terminal to know what windows exist and which workspace lives in which window.',
|
|
483
|
+
inputSchema: { type: 'object', properties: {} },
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: 'onda_window_new',
|
|
487
|
+
description: 'Open a fresh empty main window (analogue of File > New Window). Returns { windowId }. Useful when an agent needs to host a workspace in a brand new window without contaminating existing ones.',
|
|
488
|
+
inputSchema: { type: 'object', properties: {} },
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: 'onda_window_focus',
|
|
492
|
+
description: 'Bring a specific main window to the foreground (restore if minimized, raise, focus). When windowId is omitted, focuses the primary window. Returns { success, windowId }. Use after mounting a workspace remotely, or to wake Onda from a background CLI subagent.',
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
properties: {
|
|
496
|
+
windowId: { type: 'string', description: 'Target window id. Omit to focus the primary window.' },
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'onda_window_mount_workspace',
|
|
502
|
+
description: 'Mount a workspace in a specific window. Idempotent when the workspace is already there. If the workspace is currently mounted in another window, orchestrates an atomic transfer via the unmount-request flow (same path the in-app "Move workspace here?" dialog uses). Returns { success, workspaceId, windowId, transferred }. Pass `direction` to control how the new tile is spliced into the mosaic (mirrors the Cmd+P picker: Enter = down, Cmd+Enter = right).',
|
|
503
|
+
inputSchema: {
|
|
504
|
+
type: 'object',
|
|
505
|
+
properties: {
|
|
506
|
+
workspaceId: { type: 'string', description: 'Workspace to mount.' },
|
|
507
|
+
windowId: { type: 'string', description: 'Destination window id.' },
|
|
508
|
+
direction: {
|
|
509
|
+
type: 'string',
|
|
510
|
+
enum: ['down', 'right'],
|
|
511
|
+
description: 'Optional. Where to splice the new tile relative to the anchor workspace in the mosaic. Default: "down".',
|
|
512
|
+
},
|
|
513
|
+
anchorWorkspaceId: {
|
|
514
|
+
type: 'string',
|
|
515
|
+
description: 'Optional. Workspace id to anchor the split next to. Default: currently active workspace in the target window.',
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
required: ['workspaceId', 'windowId'],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: 'onda_workspace_unmount',
|
|
523
|
+
description: 'Remove a workspace from whichever window currently hosts it (drops it from that window\'s tiled mosaic and releases the mount registry slot). The workspace continues to exist in the global list — only its on-screen mounting is dropped. Returns { success, workspaceId, windowId, alreadyUnmounted }.',
|
|
524
|
+
inputSchema: {
|
|
525
|
+
type: 'object',
|
|
526
|
+
properties: {
|
|
527
|
+
workspaceId: { type: 'string', description: 'Workspace to unmount.' },
|
|
528
|
+
},
|
|
529
|
+
required: ['workspaceId'],
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
// --- Advanced terminal spawn ---
|
|
533
|
+
{
|
|
534
|
+
name: 'onda_terminal_spawn',
|
|
535
|
+
description: 'Spawn a binary inside an EXISTING pane by writing `exec bin args...` into its PTY. Preserves multi-line/quoted argv elements verbatim (each one becomes a single execve argv entry after shell quoting). Use this to launch `claude` (or any agent) with a structured prompt inside a workspace pane created via onda_workspace_add_terminal. Either paneId or workspaceId is required; if workspaceId, the first terminal pane in that workspace is used.',
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: 'object',
|
|
538
|
+
properties: {
|
|
539
|
+
paneId: { type: 'string', description: 'Target pane ID (preferred).' },
|
|
540
|
+
workspaceId: { type: 'string', description: 'Workspace ID — use when paneId is not known.' },
|
|
541
|
+
bin: { type: 'string', description: 'PATH-resolvable or absolute binary (e.g., "claude").' },
|
|
542
|
+
args: {
|
|
543
|
+
type: 'array',
|
|
544
|
+
items: { type: 'string' },
|
|
545
|
+
description: 'Arguments passed verbatim. Each element is one argv entry; embedded newlines/quotes are preserved.',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
required: ['bin'],
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
// --- High-level macro ---
|
|
552
|
+
{
|
|
553
|
+
name: 'onda_launch_session',
|
|
554
|
+
description: 'High-level macro: ensure workspace exists → mount it in target window → add a terminal pane → spawn `bin` with `args` (or `prompt` as single argv). Atomic from the agent\'s point of view. Supports placement modes: "auto" (default), "current-window", "window:<windowId>", "new-window", "ask-user" (interactive — see below).\n\n**Placement "ask-user"**: the tool does NOT proceed. Instead returns { needsDecision: true, options: [{placement, label}], workspace }. The host agent must surface the choice to the human user and re-invoke this tool with placement set to a concrete value (e.g. "window:w-abc").\n\nOn success returns { windowId, workspaceId, terminalId, paneId, pid }.',
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
properties: {
|
|
558
|
+
workspace: {
|
|
559
|
+
type: 'object',
|
|
560
|
+
description: 'Workspace reference. Provide id | name | rootPath. Set createIfMissing:true (with name+rootPath) to auto-create.',
|
|
561
|
+
properties: {
|
|
562
|
+
id: { type: 'string' },
|
|
563
|
+
name: { type: 'string' },
|
|
564
|
+
rootPath: { type: 'string' },
|
|
565
|
+
createIfMissing: { type: 'boolean' },
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
bin: { type: 'string', description: 'Binary to spawn (e.g., "claude").' },
|
|
569
|
+
args: { type: 'array', items: { type: 'string' }, description: 'Argv entries (preserved verbatim).' },
|
|
570
|
+
prompt: { type: 'string', description: 'Shortcut: passed as a single argv entry. Ignored if args is set.' },
|
|
571
|
+
placement: {
|
|
572
|
+
type: 'string',
|
|
573
|
+
description: 'auto | current-window | window:<id> | new-window | ask-user',
|
|
574
|
+
},
|
|
575
|
+
addTerminalIfNeeded: { type: 'boolean', description: 'Default true. When false, expects a pane to already exist in the workspace.' },
|
|
576
|
+
},
|
|
577
|
+
required: ['workspace', 'bin'],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
298
580
|
// --- System ---
|
|
299
581
|
{
|
|
300
582
|
name: 'onda_context',
|
|
@@ -345,10 +627,20 @@ const TOOL_MAP = {
|
|
|
345
627
|
onda_pane_close: { method: 'pane.close' },
|
|
346
628
|
onda_pane_focus: { method: 'pane.focus' },
|
|
347
629
|
// Terminal
|
|
630
|
+
// Spatial awareness
|
|
631
|
+
onda_workspace_layout: { method: 'workspace.layout' },
|
|
632
|
+
onda_window_screenshot: { method: 'window.screenshot' },
|
|
348
633
|
onda_terminal_run: { method: 'terminal.run' },
|
|
349
634
|
onda_terminal_send: { method: 'terminal.send' },
|
|
350
635
|
onda_terminal_list: { method: 'terminal.list' },
|
|
351
636
|
onda_terminal_kill: { method: 'terminal.kill' },
|
|
637
|
+
onda_terminal_read: { method: 'terminal.read' },
|
|
638
|
+
onda_terminal_subscribe: { method: 'terminal.subscribe' },
|
|
639
|
+
onda_terminal_poll: { method: 'terminal.poll' },
|
|
640
|
+
onda_terminal_unsubscribe: { method: 'terminal.unsubscribe' },
|
|
641
|
+
onda_terminal_listeners: { method: 'terminal.listeners' },
|
|
642
|
+
onda_terminal_wait_for: { method: 'terminal.waitFor' },
|
|
643
|
+
onda_terminal_send_keys: { method: 'terminal.sendKeys' },
|
|
352
644
|
// Tab
|
|
353
645
|
onda_tab_new: {
|
|
354
646
|
method: 'tab.new',
|
|
@@ -362,6 +654,13 @@ const TOOL_MAP = {
|
|
|
362
654
|
onda_tab_list: { method: 'tab.list' },
|
|
363
655
|
onda_tab_close: { method: 'tab.close' },
|
|
364
656
|
onda_tab_focus: { method: 'tab.focus' },
|
|
657
|
+
onda_tab_exec: {
|
|
658
|
+
method: 'tab.exec',
|
|
659
|
+
mapParams: (args) => ({
|
|
660
|
+
...args,
|
|
661
|
+
workspaceId: args.workspaceId || ONDA_CONTEXT.workspaceId || undefined,
|
|
662
|
+
}),
|
|
663
|
+
},
|
|
365
664
|
// Workspace
|
|
366
665
|
onda_workspace_list: { method: 'workspace.list' },
|
|
367
666
|
onda_workspace_create: { method: 'workspace.create' },
|
|
@@ -372,9 +671,30 @@ const TOOL_MAP = {
|
|
|
372
671
|
workspaceId: args.workspaceId || ONDA_CONTEXT.workspaceId || undefined,
|
|
373
672
|
cwd: args.cwd,
|
|
374
673
|
shell: args.shell,
|
|
674
|
+
waitForReady: args.waitForReady,
|
|
675
|
+
direction: args.direction,
|
|
676
|
+
relativeToPaneId: args.relativeToPaneId,
|
|
375
677
|
}),
|
|
376
678
|
},
|
|
377
679
|
onda_workspace_tile: { method: 'workspace.setLayout' },
|
|
680
|
+
onda_workspace_locate: { method: 'workspace.locate' },
|
|
681
|
+
// Window
|
|
682
|
+
onda_window_list: { method: 'window.list' },
|
|
683
|
+
onda_window_new: { method: 'window.new' },
|
|
684
|
+
onda_window_focus: { method: 'window.focus' },
|
|
685
|
+
onda_window_mount_workspace: { method: 'window.mountWorkspace' },
|
|
686
|
+
onda_workspace_unmount: { method: 'workspace.unmount' },
|
|
687
|
+
// Advanced spawn + macro
|
|
688
|
+
onda_terminal_spawn: {
|
|
689
|
+
method: 'terminal.spawnInPane',
|
|
690
|
+
mapParams: (args) => ({
|
|
691
|
+
paneId: args.paneId || ONDA_CONTEXT.paneId || undefined,
|
|
692
|
+
workspaceId: args.workspaceId || ONDA_CONTEXT.workspaceId || undefined,
|
|
693
|
+
bin: args.bin,
|
|
694
|
+
args: args.args,
|
|
695
|
+
}),
|
|
696
|
+
},
|
|
697
|
+
onda_launch_session: { method: 'launchSession' },
|
|
378
698
|
// System (onda_context handled separately - it's local, not RPC)
|
|
379
699
|
onda_status: { method: 'session.current' },
|
|
380
700
|
onda_app_info: { method: 'app.info' },
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindfullabai/onda-mcp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "MCP server for Onda terminal - control tabs, panes, terminals from AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"onda-mcp": "./dist/index.js"
|
|
9
|
+
"onda-mcp": "./dist/index.js",
|
|
10
|
+
"onda-mcp-install-skill": "./bin/install-skill.mjs"
|
|
10
11
|
},
|
|
11
12
|
"exports": {
|
|
12
13
|
".": {
|
|
@@ -33,11 +34,17 @@
|
|
|
33
34
|
"author": "Mindfull AI <hello@mindfull.ai>",
|
|
34
35
|
"license": "MIT",
|
|
35
36
|
"files": [
|
|
36
|
-
"dist"
|
|
37
|
+
"dist",
|
|
38
|
+
"bin",
|
|
39
|
+
"scripts/install-skill.mjs",
|
|
40
|
+
"skills"
|
|
37
41
|
],
|
|
38
42
|
"scripts": {
|
|
39
|
-
"build": "tsc && chmod +x dist/index.js",
|
|
40
|
-
"dev": "tsx src/index.ts"
|
|
43
|
+
"build": "tsc && chmod +x dist/index.js bin/install-skill.mjs scripts/install-skill.mjs",
|
|
44
|
+
"dev": "tsx src/index.ts",
|
|
45
|
+
"install-skill": "node scripts/install-skill.mjs",
|
|
46
|
+
"postinstall": "node scripts/install-skill.mjs --quiet || true",
|
|
47
|
+
"prepublishOnly": "npm run build"
|
|
41
48
|
},
|
|
42
49
|
"dependencies": {
|
|
43
50
|
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install (or update) the onda-mcp-usage Claude Code skill into
|
|
4
|
+
* `~/.claude/skills/onda-mcp-usage/`.
|
|
5
|
+
*
|
|
6
|
+
* Runs automatically as a `postinstall` step of @mindfullabai/onda-mcp,
|
|
7
|
+
* AND can be invoked manually:
|
|
8
|
+
* npx @mindfullabai/onda-mcp install-skill
|
|
9
|
+
* npx @mindfullabai/onda-mcp install-skill --force
|
|
10
|
+
* npx @mindfullabai/onda-mcp install-skill --skills-dir /custom/path
|
|
11
|
+
*
|
|
12
|
+
* Idempotent + version-aware: if the installed SKILL.md has the same
|
|
13
|
+
* `metadata.version` as the bundled one, skip. Use `--force` to overwrite
|
|
14
|
+
* regardless. Honors `--quiet` for silent CI runs.
|
|
15
|
+
*
|
|
16
|
+
* Failure modes are non-fatal: if we cannot write (e.g. ~/.claude doesn't
|
|
17
|
+
* exist on a CI box), we log a hint and exit 0 so `npm install` does not
|
|
18
|
+
* abort the user's workflow.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
22
|
+
import { homedir, EOL } from 'node:os';
|
|
23
|
+
import { dirname, join, resolve } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const PKG_ROOT = resolve(__dirname, '..');
|
|
28
|
+
const SOURCE_SKILL_PATH = resolve(PKG_ROOT, 'skills', 'onda-mcp-usage', 'SKILL.md');
|
|
29
|
+
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
const FLAG_FORCE = argv.includes('--force');
|
|
32
|
+
const FLAG_QUIET = argv.includes('--quiet') || process.env.ONDA_MCP_QUIET === '1';
|
|
33
|
+
|
|
34
|
+
const customSkillsDirIdx = argv.findIndex((a) => a === '--skills-dir');
|
|
35
|
+
const TARGET_SKILLS_DIR =
|
|
36
|
+
customSkillsDirIdx !== -1 && argv[customSkillsDirIdx + 1]
|
|
37
|
+
? resolve(argv[customSkillsDirIdx + 1])
|
|
38
|
+
: join(homedir(), '.claude', 'skills');
|
|
39
|
+
|
|
40
|
+
const TARGET_SKILL_DIR = join(TARGET_SKILLS_DIR, 'onda-mcp-usage');
|
|
41
|
+
const TARGET_SKILL_PATH = join(TARGET_SKILL_DIR, 'SKILL.md');
|
|
42
|
+
|
|
43
|
+
const log = (...args) => {
|
|
44
|
+
if (!FLAG_QUIET) console.log('[onda-mcp install-skill]', ...args);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract `metadata.version: x.y.z` from a SKILL.md frontmatter, or
|
|
49
|
+
* fall back to the top-level `version:` key. Returns null if absent.
|
|
50
|
+
*/
|
|
51
|
+
function extractVersion(content) {
|
|
52
|
+
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
53
|
+
if (!fm) return null;
|
|
54
|
+
const block = fm[1];
|
|
55
|
+
const metaVer = block.match(/^\s*version:\s*['"]?([^'"\n]+?)['"]?\s*$/m);
|
|
56
|
+
if (metaVer) return metaVer[1].trim();
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function main() {
|
|
61
|
+
if (!existsSync(SOURCE_SKILL_PATH)) {
|
|
62
|
+
log(`source skill missing at ${SOURCE_SKILL_PATH} — nothing to install`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sourceContent = readFileSync(SOURCE_SKILL_PATH, 'utf8');
|
|
67
|
+
const sourceVersion = extractVersion(sourceContent);
|
|
68
|
+
|
|
69
|
+
// Target dir may not exist (Claude Code not installed on this box).
|
|
70
|
+
if (!existsSync(dirname(TARGET_SKILLS_DIR))) {
|
|
71
|
+
log(
|
|
72
|
+
`~/.claude not found — Claude Code does not appear to be installed.${EOL}` +
|
|
73
|
+
' Skipping skill install. Run `npx @mindfullabai/onda-mcp install-skill` later if needed.',
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let installedVersion = null;
|
|
79
|
+
if (existsSync(TARGET_SKILL_PATH)) {
|
|
80
|
+
try {
|
|
81
|
+
installedVersion = extractVersion(readFileSync(TARGET_SKILL_PATH, 'utf8'));
|
|
82
|
+
} catch {
|
|
83
|
+
installedVersion = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!FLAG_FORCE && installedVersion && sourceVersion && installedVersion === sourceVersion) {
|
|
88
|
+
log(`already up to date (v${installedVersion}) — skipping. Use --force to overwrite.`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
mkdirSync(TARGET_SKILL_DIR, { recursive: true });
|
|
94
|
+
writeFileSync(TARGET_SKILL_PATH, sourceContent, 'utf8');
|
|
95
|
+
const verLabel = sourceVersion ? `v${sourceVersion}` : '(unversioned)';
|
|
96
|
+
if (installedVersion) {
|
|
97
|
+
log(`updated ${installedVersion} -> ${sourceVersion ?? '?'} at ${TARGET_SKILL_PATH}`);
|
|
98
|
+
} else {
|
|
99
|
+
log(`installed ${verLabel} at ${TARGET_SKILL_PATH}`);
|
|
100
|
+
}
|
|
101
|
+
// Sanity: confirm write
|
|
102
|
+
const written = statSync(TARGET_SKILL_PATH);
|
|
103
|
+
if (written.size === 0) {
|
|
104
|
+
log('WARNING: wrote 0 bytes — install may have failed silently');
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
log(`install failed: ${err.message}`);
|
|
109
|
+
log(' Hint: run with --skills-dir <path> to redirect, or fix permissions on ~/.claude/skills/');
|
|
110
|
+
// Non-fatal during postinstall to avoid breaking `npm install`.
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main();
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onda-mcp-usage
|
|
3
|
+
description: Best practices for driving Onda (terminal emulator) from Claude Code via the `onda` MCP server (`mcp__onda__*`). Covers workspaces/windows/panes/terminals + buffer reading (read/subscribe/poll/wait_for/send_keys) + spatial awareness (layout/screenshot). Use whenever you call any `mcp__onda__*` tool or the user mentions Onda, pane, workspace, terminal listener, presence badge, inception loop (Claude controlling Onda from inside Onda). Triggers — onda mcp, drive onda, control onda terminal, show pane, workspace layout, onda screenshot, kai watcher, terminal listener.
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.2.0
|
|
6
|
+
source: '@mindfullabai/onda-mcp'
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Onda MCP usage skill
|
|
10
|
+
|
|
11
|
+
This skill tells you **how to correctly use the 38 `mcp__onda__*` tools** exposed by the `onda-mcp` server. It is installed alongside the MCP server. Without this guide you hit non-obvious pitfalls (subscribe AFTER run loses output, `down`/`right` mapping inverted pre-fix, UI cache refresh).
|
|
12
|
+
|
|
13
|
+
## Mental model
|
|
14
|
+
|
|
15
|
+
You (Claude Code) run inside **a terminal inside Onda**. The `onda-mcp` server talks to the Onda host app via JSON-RPC over UDS at `~/.config/onda/onda.sock`. **Inception loop**: every action through `mcp__onda__*` mutates the app you're running inside. Treat pane/workspace state as **shared mutable** with the user — do not assume it stays the same.
|
|
16
|
+
|
|
17
|
+
Onda has three layout primitives:
|
|
18
|
+
- **Window**: Electron BrowserWindow. N workspaces can coexist in one Window in tiled mode.
|
|
19
|
+
- **Workspace**: root folder + collection of panes. Mounted into a Window.
|
|
20
|
+
- **Pane**: container with `contentType: terminal | editor | diff`. Terminals have a stable `terminalId` (survives split/merge).
|
|
21
|
+
|
|
22
|
+
Internal workspace layout = mosaic-component tree. Read it via `onda_workspace_layout`.
|
|
23
|
+
|
|
24
|
+
## When to use what — cheat sheet
|
|
25
|
+
|
|
26
|
+
| Goal | Tool |
|
|
27
|
+
|---|---|
|
|
28
|
+
| "Which workspaces exist" | `onda_workspace_list` |
|
|
29
|
+
| "Which Windows are open" | `onda_window_list` |
|
|
30
|
+
| "How is workspace X laid out" | `onda_workspace_layout` |
|
|
31
|
+
| "I want to see what the user sees" | `onda_window_screenshot` |
|
|
32
|
+
| "Open a new terminal right/below pane X" | `onda_workspace_add_terminal` with `direction` + `relativeToPaneId` |
|
|
33
|
+
| "Read what the terminal has printed so far" | `onda_terminal_read` (no subscribe needed) |
|
|
34
|
+
| "I want to receive every new output chunk" | `subscribe` + `poll` loop |
|
|
35
|
+
| "Wait until the build finishes" | `onda_terminal_wait_for` with regex |
|
|
36
|
+
| "Press Ctrl+C / Up / Esc" | `onda_terminal_send_keys` (NOT `terminal_send` with `"\x03"`!) |
|
|
37
|
+
| "Type a command and run it" | `onda_terminal_run` (text + \n) |
|
|
38
|
+
| "Detach listener when done" | `onda_terminal_unsubscribe` (ALWAYS) |
|
|
39
|
+
| "Spawn Claude Code in a new pane with a prompt" | `onda_terminal_spawn` with `bin=claude`, `args=[prompt]` |
|
|
40
|
+
|
|
41
|
+
## Pattern: working with terminal buffer (reads)
|
|
42
|
+
|
|
43
|
+
**Anti-pattern**: call `terminal.run` and THEN `terminal.read`. The tap is created lazily on first `read`/`subscribe`, and PTY data emitted before the tap exists is **lost**.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
✗ run → read (you lose the run's output)
|
|
47
|
+
✓ read|subscribe → run → read|poll (captures everything)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Ring buffer size:
|
|
51
|
+
- **200 KB** when no listener (`read`-only mode)
|
|
52
|
+
- **1 MB** when at least one `subscribe` is active
|
|
53
|
+
|
|
54
|
+
For high-throughput output (`find /`, `tail -F` on logs) the ring saturates. Use `wait_for` with a precise pattern instead of subscribe + scan.
|
|
55
|
+
|
|
56
|
+
## Pattern: subscribe + poll loop
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
sub = subscribe(id, listener="kai-watcher")
|
|
60
|
+
loop {
|
|
61
|
+
res = poll(sub.sessionId, timeoutMs=15000)
|
|
62
|
+
for chunk in res.chunks: process(chunk.data)
|
|
63
|
+
}
|
|
64
|
+
unsubscribe(sub.sessionId)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`poll` is **long-poll**: unblocks on first `data` event or on timeout. Immediate wake-up on new output. Cursor advances only on successful poll — no replay.
|
|
68
|
+
|
|
69
|
+
**Mandatory cleanup**: call `unsubscribe` when done. Automatic TTL is 30 min but don't rely on it.
|
|
70
|
+
|
|
71
|
+
## Pattern: scriptable terminal automation (tmux-style)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
sub = subscribe(id, "kai-script") # optional, for log
|
|
75
|
+
send_keys(id, ["echo BUILD_START", "Enter"])
|
|
76
|
+
wait_for(id, /BUILD_START/) # sync barrier
|
|
77
|
+
send_keys(id, ["npm test", "Enter"])
|
|
78
|
+
wait_for(id, /PASS|FAIL|error/, timeoutMs=120000)
|
|
79
|
+
send_keys(id, ["Ctrl+C"]) # cleanup
|
|
80
|
+
unsubscribe(sub.sessionId)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Keysyms supported by `send_keys`: `Enter`, `Tab`, `Escape`, `Space`, `Backspace`, `Delete`, `Up/Down/Left/Right`, `Home/End/PageUp/PageDown`, `Insert`, `Ctrl+<letter>`, `F1`-`F12`. Anything else is sent as literal text (useful for `["echo hello", "Enter"]`).
|
|
84
|
+
|
|
85
|
+
`send_keys` ≠ `terminal_send`: the first maps semantic keysyms → ANSI bytes. The second sends raw text. For `Ctrl+C` ALWAYS use `send_keys(["Ctrl+C"])`.
|
|
86
|
+
|
|
87
|
+
## Pattern: spatial-aware placement
|
|
88
|
+
|
|
89
|
+
**Anti-pattern**: spawning panes via `add_terminal` blindly and letting them land randomly. The window grows rightward forever.
|
|
90
|
+
|
|
91
|
+
**Correct pattern**:
|
|
92
|
+
```
|
|
93
|
+
layout = workspace_layout(workspaceId)
|
|
94
|
+
# layout.paneIds: [...]; layout.activePaneId; layout.viewport
|
|
95
|
+
# Decide: stack down for continuous output, split right for parallel-watch
|
|
96
|
+
new = workspace_add_terminal(workspaceId,
|
|
97
|
+
direction="down",
|
|
98
|
+
relativeToPaneId=layout.activePaneId)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`direction` mapping:
|
|
102
|
+
- `right` / `left` → side-by-side (left-right pair)
|
|
103
|
+
- `down` / `up` → stacked (top-bottom pair)
|
|
104
|
+
- `horizontal` = stacked (synonym of `down`)
|
|
105
|
+
- `vertical` = side-by-side (synonym of `right`)
|
|
106
|
+
|
|
107
|
+
Use `down` when the new pane will emit continuous output (logs, build). Use `right` for parallel work terminals.
|
|
108
|
+
|
|
109
|
+
## Pattern: visual verification
|
|
110
|
+
|
|
111
|
+
When you need to visually confirm an action succeeded (layout changed, badge appeared, modal opened) use `window_screenshot`:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
screenshot = window_screenshot(windowId, format="jpeg", quality=72, dataUrl=false)
|
|
115
|
+
# returns path; use Read on the image
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Default `dataUrl=true` returns inline base64 (~1-2 MB), `dataUrl=false` writes a tempfile that `Read` mounts as multimodal image — preferred to avoid wasting context.
|
|
119
|
+
|
|
120
|
+
## Pattern: launching Claude Code in a pane (inception)
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
pane = workspace_add_terminal(workspaceId="...", direction="right")
|
|
124
|
+
subscribe(pane.terminalId, "kai-watcher") # BEFORE launch
|
|
125
|
+
terminal_spawn(pane.paneId, bin="claude", args=["--dangerously-skip-permissions", "prompt..."])
|
|
126
|
+
# Claude TUI shows "Trust this folder?" prompt
|
|
127
|
+
send_keys(pane.terminalId, ["Enter"]) # confirm default highlighted (1. Yes)
|
|
128
|
+
# From here Claude works — use wait_for with expected completion pattern
|
|
129
|
+
wait_for(pane.terminalId, /===KAI_CHECK_DONE===/, timeoutMs=120000)
|
|
130
|
+
unsubscribe(...)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Claude TUI gotcha**: the stream is VERY noisy (100+ chunks/sec of ANSI spinners). Prefer `wait_for(pattern)` over continuous `poll`. For visual debugging use `screenshot` every 30s, NOT text buffer reads.
|
|
134
|
+
|
|
135
|
+
## Pattern: drive a remote Claude Code session (auto-pilot)
|
|
136
|
+
|
|
137
|
+
Use case: Alita just spawned Kai in an adjacent Onda tab and wants to **guide it autonomously** without Mario clicking anything. Pattern validated 21 May 2026.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
# 1. Spawn (tab_exec or terminal_spawn)
|
|
141
|
+
tab_exec(bin="claude", args=["--model","sonnet"], cwd="~/Projects/...")
|
|
142
|
+
|
|
143
|
+
# 2. Find the terminal ID (filter by workspaceId)
|
|
144
|
+
terms = terminal_list(workspaceId="...")
|
|
145
|
+
kai_id = terms[-1].id # last spawned
|
|
146
|
+
|
|
147
|
+
# 3. Handle trust prompt if first time in that folder
|
|
148
|
+
wait_for(kai_id, "trust this folder", timeoutMs=10000)
|
|
149
|
+
send_keys(kai_id, ["1", "Enter"])
|
|
150
|
+
|
|
151
|
+
# 4. Wait for boot completion (welcome screen / inbox banner)
|
|
152
|
+
wait_for(kai_id, "Welcome back|Inbox|MVD\\?", timeoutMs=15000)
|
|
153
|
+
|
|
154
|
+
# 5. Type the prompt in the textbox
|
|
155
|
+
terminal_run(kai_id, "You are Kai. Proceed like this: /read msg-... then ...")
|
|
156
|
+
|
|
157
|
+
# 6. CRITICAL: submit the prompt — terminal_run adds \n but Claude TUI treats \n as newline buffer, NOT submit
|
|
158
|
+
send_keys(kai_id, ["Enter"])
|
|
159
|
+
|
|
160
|
+
# 7. Wait for actual work to start
|
|
161
|
+
wait_for(kai_id, "tokens|reading|esc to interrupt", timeoutMs=30000)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Critical submit gotcha**: `terminal_run "msg"` writes `msg\n` but Claude's TUI **interprets `\n` as newline in the multi-line buffer**, not as Submit. ALWAYS follow with `send_keys(["Enter"])` to actually submit. Without it, the prompt stays in editing mode forever.
|
|
165
|
+
|
|
166
|
+
**Empty buffer gotcha**: `terminal_read` on a freshly spawned terminal returns `content:""` because the passive tap attaches lazily on first call. Use `wait_for` with regex on known output patterns (e.g. "tokens", "Welcome", "Tips") instead of assuming `read` shows everything immediately.
|
|
167
|
+
|
|
168
|
+
## Pattern: observe remote session completion
|
|
169
|
+
|
|
170
|
+
After `drive remote session` you need to know WHEN the remote task finishes or hits a milestone, without blocking the current session with a multi-hour `wait_for`.
|
|
171
|
+
|
|
172
|
+
Decision rule for choosing the pattern:
|
|
173
|
+
|
|
174
|
+
| Case | Pattern |
|
|
175
|
+
|------|---------|
|
|
176
|
+
| Task <5 min, can wait blocked | `terminal_wait_for` direct, single-shot |
|
|
177
|
+
| Long task (>15 min) with bus available | B. Bus inbox check (target uses `/reply`) |
|
|
178
|
+
| Long task, want intermediate milestones | A. Marker pattern + `ScheduleWakeup` |
|
|
179
|
+
| Multi-hour task, automatic polling | D. `/loop <interval> /inbox` |
|
|
180
|
+
| System-wide background daemon | C. fswatch + osascript (roadmap, not implemented) |
|
|
181
|
+
|
|
182
|
+
### A. Marker pattern + ScheduleWakeup
|
|
183
|
+
|
|
184
|
+
Instruct the target session to print explicit markers at milestones:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
terminal_run(kai_id, "Print markers: ===KAI_PHASE1_DONE===, ===KAI_PHASE2_DONE===, ===KAI_ALL_DONE===")
|
|
188
|
+
send_keys(kai_id, ["Enter"])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Then periodically:
|
|
192
|
+
```
|
|
193
|
+
ScheduleWakeup(delaySeconds=900, prompt="check Kai status")
|
|
194
|
+
# on wakeup:
|
|
195
|
+
out = terminal_read(kai_id, lines=200)
|
|
196
|
+
if "===KAI_ALL_DONE===" in out: close coordination
|
|
197
|
+
elif "===KAI_PHASE2_DONE===" in out: log progress, wakeup again
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Pro: granular, intermediate milestones, no extra infra.
|
|
201
|
+
Con: depends on target actually printing them; markers may fall out of the 200KB ring buffer.
|
|
202
|
+
|
|
203
|
+
### B. Bus inbox check (canonical for agent-bus)
|
|
204
|
+
|
|
205
|
+
Leverage the bus. Target ends task with `/reply <msg-id> response`. Lead receives in `~/.agent-bus/inboxes/<lead>/`. Lead sees the response on next `/inbox` or via SessionStart hook.
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
terminal_run(kai_id, "When done, run /reply <msg-id> response with summary + artifact_refs")
|
|
209
|
+
|
|
210
|
+
# Passive check:
|
|
211
|
+
# - automatic via SessionStart hook agent-bus-load.sh
|
|
212
|
+
# - manual: ls ~/.agent-bus/inboxes/alita/ | grep msg-
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Pro: native, zero infra, audit trail in thread, independent of terminal PID/buffer.
|
|
216
|
+
Con: on/off only (done or not), no intermediate milestones.
|
|
217
|
+
|
|
218
|
+
Combinable with A: markers in terminal for milestones + final `/reply` via bus. Best of both. (Pattern used 21 May 2026 with Kai on Vera's todoist sdk migration brief.)
|
|
219
|
+
|
|
220
|
+
### D. Recurring /loop
|
|
221
|
+
|
|
222
|
+
For multi-hour tasks with automatic check:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
/loop 15m /inbox
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The loop checks my inbox every 15 min. Combine with B.
|
|
229
|
+
|
|
230
|
+
Pro: automatic, lightweight, non-blocking.
|
|
231
|
+
Con: prompt cache miss every 15 min (cache threshold 5 min), non-zero token cost. For tasks <2h prefer A or passive B.
|
|
232
|
+
|
|
233
|
+
### Observer anti-patterns
|
|
234
|
+
|
|
235
|
+
- `terminal_wait_for` with multi-hour `timeoutMs`: blocks current session, you lose useful context.
|
|
236
|
+
- Live subscribe to Claude TUI: 100 chunks/sec, saturates listener, burns budget.
|
|
237
|
+
- `terminal_read` polling with sleep <60s: cache miss + noise.
|
|
238
|
+
- Relying on marker pattern WITHOUT explicitly instructing the target to print them. The target doesn't produce them on its own.
|
|
239
|
+
|
|
240
|
+
**Dirty ANSI buffer gotcha**: read output contains escape sequences (`[?6n`, `[H`, etc). Don't parse the buffer — only use `wait_for` with regex on known tokens, or `screenshot` if you need visual verification.
|
|
241
|
+
|
|
242
|
+
## Cleanup discipline
|
|
243
|
+
|
|
244
|
+
**Always**:
|
|
245
|
+
- `unsubscribe(sessionId)` when done reading
|
|
246
|
+
- `pane_close(id)` for panes you created as test/scratch (NOT existing user panes — assume they're Mario's)
|
|
247
|
+
- Prefix listener names with your identifier (`kai-`, `alita-`, `ci-`) for UI badge legibility
|
|
248
|
+
|
|
249
|
+
**Never close**:
|
|
250
|
+
- The user's original panes
|
|
251
|
+
- The user's workspaces (`workspace_focus` to switch is OK, but no `workspace_delete`)
|
|
252
|
+
- The user's windows (`window_new` is OK, never close existing ones)
|
|
253
|
+
|
|
254
|
+
## Anti-pattern catalog
|
|
255
|
+
|
|
256
|
+
1. **Subscribe to Claude TUI**: nope. Generates 100 chunks/sec of spinner. Use `read` at intervals + `wait_for` for known patterns.
|
|
257
|
+
2. **`terminal_send` with raw bytes** (`"\x03"` for Ctrl+C): nope. Use `send_keys(["Ctrl+C"])`.
|
|
258
|
+
3. **`workspace_add_terminal` without prior `workspace_layout`**: blind placement. Append-right quickly becomes unreadable.
|
|
259
|
+
4. **Listener without unsubscribe**: leak for 30 min TTL. UI badge stays. Clean up.
|
|
260
|
+
5. **`window_new`** when the user just wants "one more workspace on the right": NO — the user prefers **mounting the workspace as a tile in the existing window** (today the MCP tool for this is missing → use manual click flow with `tell me when you clicked`).
|
|
261
|
+
6. **Assuming `app_info.name === "Onda-dev"`** distinguishes dev vs prod: NO. Use `app_info.path` or an explicit flag.
|
|
262
|
+
7. **`terminal_run` alone to submit a prompt to Claude TUI**: NO. The TUI treats `\n` as newline buffer. You must always follow with `send_keys(["Enter"])` to actually submit. (Validated 21 May 2026 spawning a dedicated Kai.)
|
|
263
|
+
8. **Spawning Kai and then waiting for Mario to click and type**: anti-pattern. If you spawned the session, you drive it to "Kai is working" (pattern "drive a remote Claude Code session"). Mario shouldn't be a human postman in your agent team.
|
|
264
|
+
|
|
265
|
+
## Useful knowledge
|
|
266
|
+
|
|
267
|
+
- Pane header listener badge: blinks blue, click opens popover with "kick" to terminate the session
|
|
268
|
+
- Onda-dev runs with bundle id `com.mariomosca.onda.dev` (sandboxed, no collision with prod)
|
|
269
|
+
- Onda uses a **single UDS socket**, `~/.config/onda/onda.sock`. Only one Onda instance answers MCP at a time (the first to bind). If `app_info` shows odd data, you may be talking to a different instance.
|
|
270
|
+
- The MCP server holds the socket binding until the master Onda app exits — restarting Onda via Cmd+Q + relaunch is how you "promote" another instance to master.
|
|
271
|
+
|
|
272
|
+
## Tool reference (38 total)
|
|
273
|
+
|
|
274
|
+
Application:
|
|
275
|
+
- `onda_app_info` — version, pid, paths
|
|
276
|
+
- `onda_app_ping` — health check
|
|
277
|
+
|
|
278
|
+
Window (Electron BrowserWindow):
|
|
279
|
+
- `onda_window_list` — N windows + workspaces[] per window
|
|
280
|
+
- `onda_window_new` — opens a new empty Window (use with caution, the user prefers same window)
|
|
281
|
+
- `onda_window_screenshot` — PNG/JPEG compositor snapshot (see what the user sees)
|
|
282
|
+
|
|
283
|
+
Workspace:
|
|
284
|
+
- `onda_workspace_list` — all workspaces + `mountedIn`
|
|
285
|
+
- `onda_workspace_locate` — find by name/id/rootPath
|
|
286
|
+
- `onda_workspace_create` — create new (rootPath required)
|
|
287
|
+
- `onda_workspace_focus` — switch in focused window
|
|
288
|
+
- `onda_workspace_layout` — mosaic tree + paneIds + activePaneId + viewport
|
|
289
|
+
- `onda_workspace_add_terminal` — spawn pane with optional direction
|
|
290
|
+
- `onda_workspace_tile` — set layout (split-h, split-v, quad)
|
|
291
|
+
- `onda_workspace_setLayout` — set custom layout
|
|
292
|
+
|
|
293
|
+
Pane:
|
|
294
|
+
- `onda_pane_list` — panes in window/workspace
|
|
295
|
+
- `onda_pane_focus` — focus pane
|
|
296
|
+
- `onda_pane_split` / `vsplit` / `hsplit` — low-level split (prefer `workspace_add_terminal` with `direction`)
|
|
297
|
+
- `onda_pane_close` — close pane
|
|
298
|
+
|
|
299
|
+
Tab (legacy, before tiled-mode-first model):
|
|
300
|
+
- `onda_tab_new` / `list` / `close` / `focus` / `exec`
|
|
301
|
+
|
|
302
|
+
Terminal (control plane):
|
|
303
|
+
- `onda_terminal_list` — active terminals
|
|
304
|
+
- `onda_terminal_spawn` — exec binary (e.g. `claude`) inside existing pane
|
|
305
|
+
- `onda_terminal_send` — write raw text (NO newline)
|
|
306
|
+
- `onda_terminal_run` — send + newline
|
|
307
|
+
- `onda_terminal_resize` — cols/rows
|
|
308
|
+
- `onda_terminal_kill` — terminate PTY
|
|
309
|
+
- `onda_terminal_focus` — UI focus
|
|
310
|
+
|
|
311
|
+
Terminal (read/listen plane, M+1 2026-05-21):
|
|
312
|
+
- `onda_terminal_read` — snapshot ring buffer
|
|
313
|
+
- `onda_terminal_subscribe` — attach listener, return sessionId
|
|
314
|
+
- `onda_terminal_poll` — long-poll new chunks
|
|
315
|
+
- `onda_terminal_unsubscribe` — detach
|
|
316
|
+
- `onda_terminal_listeners` — who is listening to this terminal
|
|
317
|
+
- `onda_terminal_wait_for` — block on regex match
|
|
318
|
+
- `onda_terminal_send_keys` — semantic keysym (Ctrl+C, Up, F5, ...)
|
|
319
|
+
|
|
320
|
+
Session:
|
|
321
|
+
- `onda_session_current` — current CLI consumer session
|
|
322
|
+
- `onda_launchSession` — atomic macro "open workspace + add terminal + spawn bin"
|
|
323
|
+
|
|
324
|
+
## Versioning
|
|
325
|
+
|
|
326
|
+
Skill version: see `metadata.version` in frontmatter. Bump when you add/remove patterns or tools. To reinstall the latest version, run `npx @mindfullabai/onda-mcp install-skill` (TODO) or copy manually from `<onda-mcp-repo>/skills/onda-mcp-usage/`.
|