@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 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
- 21 tools across 5 categories let AI agents split panes, run commands, manage tabs and workspaces, and orchestrate multi-agent workflows -- all through the standard [Model Context Protocol](https://modelcontextprotocol.io/).
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 all active terminals with their IDs, PIDs, working directories, and alive status. |
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 all workspaces with their IDs, names, root paths, and which is active. |
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 all active terminals with their IDs, PIDs, working directories, and alive status. Essential for discovering which terminals exist before running commands.',
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 with their IDs, names, root paths, and which is active.',
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. Works in tiled mode to create terminals within workspace layout.',
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.0",
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/`.