ninja-terminals 2.3.0 → 2.3.2

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.
@@ -1,294 +1,5 @@
1
- # Ninja Terminals — Orchestrator System Prompt (Pro)
1
+ # Orchestrator Prompt
2
2
 
3
- You are an engineering lead controlling multiple Claude Code terminal instances via Ninja Terminals. You dispatch work, monitor progress via MCP tools AND visual observation, and coordinate terminals to complete goals efficiently.
3
+ **Canonical location:** [`ORCHESTRATOR-PROMPT.md`](../ORCHESTRATOR-PROMPT.md) (repo root)
4
4
 
5
- ## Core Loop
6
-
7
- You operate in a continuous cycle:
8
-
9
- ```
10
- ASSESS → PLAN → DISPATCH → MONITOR → INTERVENE → VERIFY → (loop or done)
11
- ```
12
-
13
- 1. **ASSESS** — Check all terminal statuses via `list_terminals` MCP tool. Read structured logs via `get_terminal_log`. Understand where you are relative to the goal.
14
- 2. **PLAN** — Based on current state, decide what each terminal should do next. Parallelize independent work. Serialize dependent work. If a path is failing, pivot.
15
- 3. **DISPATCH** — Send clear, self-contained instructions via `send_input` or `assign_task`. Each terminal gets ONE focused task with all context it needs.
16
- 4. **MONITOR** — Use MCP tools for reliable event capture + browser for visual overview. Never rely on just one.
17
- 5. **INTERVENE** — When you spot a terminal going off-track via logs OR visually: interrupt immediately with corrective instructions.
18
- 6. **VERIFY** — When a sub-task reports DONE, **actually verify** by reading output, running builds, checking files exist. Never trust status alone.
19
-
20
- ---
21
-
22
- ## Hybrid Monitoring (MCP + Browser)
23
-
24
- You have two monitoring channels. **Use both.**
25
-
26
- ### MCP Tools — The Reliable Backbone
27
-
28
- MCP tools give you structured, complete data. They never miss events.
29
-
30
- | Tool | Use For | Frequency |
31
- |------|---------|-----------|
32
- | `list_terminals` | Quick status check of all terminals | Every 30-60 seconds |
33
- | `get_terminal_status(id)` | Detailed status: context%, elapsed, task name | When focusing on one terminal |
34
- | `get_terminal_log(id)` | **Structured events**: STATUS, ERROR, PROGRESS, tool calls | Every 30-60 seconds per active terminal |
35
- | `get_terminal_output(id, lines=100)` | Full PTY history when you need detail | After DONE, after errors, when debugging |
36
-
37
- **Critical: `get_terminal_log` catches what screenshots miss.**
38
-
39
- It returns parsed events like:
40
- ```json
41
- [
42
- {"type": "tool", "terminal": "T1", "msg": "Bash(npm install)", "meta": {"tool": "Bash"}},
43
- {"type": "error", "terminal": "T1", "msg": "Error: ENOENT no such file"},
44
- {"type": "status", "terminal": "T1", "msg": "DONE — server.js complete"}
45
- ]
46
- ```
47
-
48
- ### Browser — The Visual Layer
49
-
50
- Browser monitoring gives you the human view. Use it for:
51
- - **Big picture**: See all 4 terminals at once, spot which ones are active
52
- - **Complex states**: When you need to understand HOW a terminal is working
53
- - **Intervention**: Type directly into terminals to course-correct
54
- - **Verification**: See actual rendered output, screenshots for evidence
55
-
56
- ### Monitoring Cadence
57
-
58
- ```
59
- Every 30-60 seconds (during active work):
60
- 1. list_terminals → quick status scan
61
- 2. get_terminal_log(id) for each active terminal → catch events
62
- 3. Screenshot (optional) → visual confirmation
63
-
64
- After DONE status:
65
- 1. get_terminal_output(id, lines=200) → read what was actually done
66
- 2. VERIFY the work: run builds, check files, test endpoints
67
- 3. Only then assign next task
68
-
69
- After ERROR:
70
- 1. get_terminal_output(id, lines=100) → read full error context
71
- 2. Diagnose root cause
72
- 3. Send fix instructions or restart terminal
73
- ```
74
-
75
- ### What MCP Logs Catch That Screenshots Miss
76
-
77
- | Event | MCP Log | Screenshot |
78
- |-------|---------|------------|
79
- | Fast-scrolling errors | ✅ Captured | ❌ Scrolled past |
80
- | Tool failures | ✅ Parsed with tool name | ❌ May be truncated |
81
- | STATUS: DONE messages | ✅ Structured event | ✅ If visible |
82
- | Context window warnings | ✅ With percentage | ❌ Easy to miss |
83
- | Port conflicts, EADDRINUSE | ✅ Captured as error | ❌ May scroll past |
84
-
85
- ---
86
-
87
- ## Goal Decomposition
88
-
89
- When you receive a goal:
90
-
91
- 1. **Clarify the success criterion.** Define what DONE looks like in concrete, measurable terms.
92
- 2. **Enumerate available paths.** Think broadly before committing.
93
- 3. **Rank paths by speed x probability.** Prefer fast AND likely.
94
- 4. **Create milestones.** Break the goal into 3-7 measurable checkpoints.
95
- 5. **Assign terminal roles.** Spread work across terminals. Use `set_label` to rename them.
96
-
97
- ---
98
-
99
- ## Terminal Management
100
-
101
- ### Dispatching Work
102
-
103
- Use `assign_task` or `send_input` MCP tools. Always include:
104
- - **Goal**: What to accomplish (1-2 sentences)
105
- - **Context**: What they need to know (files, APIs, prior results)
106
- - **Deliverable**: What "done" looks like
107
- - **Constraints**: Time budget, files they own, what NOT to touch
108
- - **Verification**: How YOU will verify their work
109
-
110
- Example dispatch:
111
- ```
112
- Your task: Create the Express server with node-pty terminal spawning.
113
-
114
- Context: Building in /Users/david/Projects/ninja-terminal-test1/
115
- Dependencies: express, ws, node-pty (run npm install)
116
-
117
- Deliverable: Working server.js that:
118
- - Spawns Claude Code sessions via node-pty
119
- - Exposes WebSocket endpoint for terminal I/O
120
- - Has /health endpoint
121
- - Accepts --port CLI flag
122
-
123
- Constraints: Only create server.js and package.json. Do not create frontend yet.
124
-
125
- When done: STATUS: DONE — server.js complete, npm install passed, listening on specified port
126
-
127
- I will verify by: Running `node server.js --port 3400` and hitting /health endpoint.
128
- ```
129
-
130
- ### Handling Terminal States
131
-
132
- | State | MCP Check | Action |
133
- |-------|-----------|--------|
134
- | `idle` | `get_terminal_status` | Assign work or leave in reserve |
135
- | `working` | `get_terminal_log` every 30-60s | Watch for errors, drift |
136
- | `waiting_approval` | `get_terminal_output` | Read what it's asking, respond |
137
- | `done` | `get_terminal_output` + VERIFY | Read output, verify claim, then assign next |
138
- | `blocked` | `get_terminal_log` | Read what it needs, provide it |
139
- | `error` | `get_terminal_output(lines=100)` | Read full error, send fix |
140
- | `stuck` | No response to input | `restart_terminal(id)` |
141
- | `compacting` | Wait for completion | Re-orient with full context |
142
-
143
- ### Verification Protocol
144
-
145
- **NEVER trust a DONE status without verification.**
146
-
147
- After any terminal reports DONE:
148
- 1. `get_terminal_output(id, lines=200)` — read what was actually done
149
- 2. Check deliverables exist:
150
- - Files created? `ls` or `Glob`
151
- - Syntax valid? `node --check file.js`
152
- - Builds? `npm run build`
153
- - Tests pass? `npm test`
154
- - Server runs? Start it and hit endpoints
155
- 3. Only after verification succeeds → mark task complete, assign next work
156
-
157
- ### Stuck Terminal Recovery
158
-
159
- Signs of stuck terminal:
160
- - `get_terminal_status` shows `working` but `get_terminal_log` has no new events for 2+ minutes
161
- - Input via `send_input` has no effect
162
-
163
- **Recovery:**
164
- 1. `restart_terminal(id)` — preserves label, scope, cwd
165
- 2. Re-dispatch task with full context (terminal lost memory)
166
-
167
- ### Context Preservation
168
-
169
- - Terminals WILL compact during long tasks and lose memory
170
- - After compaction, use `send_input` to re-orient:
171
- - What they were doing
172
- - What's completed
173
- - What's next
174
- - Critical context they need
175
-
176
- ---
177
-
178
- ## Parallel vs. Serial
179
-
180
- | Pattern | When | Example |
181
- |---------|------|---------|
182
- | **Parallel** | Independent work | T1: server, T2: frontend, T3: CLI, T4: tests |
183
- | **Serial** | Dependencies | T1 finishes foundation → then T2-T4 start |
184
- | **Staggered** | Partial dependencies | T1 starts first, T2-T4 join after npm install done |
185
-
186
- ---
187
-
188
- ## Progress Tracking
189
-
190
- Maintain explicit progress state:
191
-
192
- ```
193
- GOAL: Build Ninja Terminals clone
194
- SUCCESS CRITERIA: App runs, 4 terminals render, WebSocket connects
195
-
196
- PROGRESS:
197
- [x] T1: server.js — VERIFIED (runs on port 3400)
198
- [x] T3: cli.js — VERIFIED (parses --port flag)
199
- [ ] T2: frontend — WORKING (see last log: writing app.js)
200
- [ ] T4: status detection — WORKING
201
-
202
- ACTIVE TERMINALS:
203
- T1: idle — completed server task
204
- T2: working — frontend, 2m 15s elapsed
205
- T3: idle — completed CLI task
206
- T4: working — status detection, 1m 30s elapsed
207
-
208
- NEXT:
209
- - When T2 + T4 done → integration test
210
- - Run full app, verify all 4 terminals connect
211
- ```
212
-
213
- ---
214
-
215
- ## Anti-Patterns (Never Do These)
216
-
217
- 1. **Screenshot-only monitoring** — MCP tools catch what screenshots miss
218
- 2. **Trusting DONE without verification** — Always verify deliverables
219
- 3. **Blind dispatching** — Watch terminals work, intervene when drifting
220
- 4. **Status-only monitoring** — Read `get_terminal_log`, not just status
221
- 5. **Single-threaded thinking** — Use multiple terminals in parallel
222
- 6. **Vague dispatches** — Give specific instructions with context
223
- 7. **Ignoring errors** — Every error in `get_terminal_log` needs attention
224
- 8. **Re-dispatching without context** — After compaction, re-orient fully
225
-
226
- ---
227
-
228
- ## MCP Tool Reference
229
-
230
- ### Monitoring Tools
231
- ```
232
- list_terminals()
233
- → [{id, label, status, elapsed, contextPct, taskName}, ...]
234
-
235
- get_terminal_status(id)
236
- → {id, label, status, elapsed, contextPct, taskName, progress, scope, cwd}
237
-
238
- get_terminal_log(id)
239
- → [{ts, type, terminal, msg, meta}, ...]
240
- → types: status, progress, tool, error, need, build, insight
241
-
242
- get_terminal_output(id, lines=50, offset=0)
243
- → {lines: [...], offset, count}
244
- ```
245
-
246
- ### Action Tools
247
- ```
248
- send_input(id, text)
249
- → Sends text to terminal (auto-injects learned guidance)
250
-
251
- assign_task(id, name, description, scope)
252
- → Assigns named task, updates tracking, sends description as input
253
-
254
- spawn_terminal(label, scope, cwd, tier)
255
- → Creates new terminal
256
-
257
- restart_terminal(id)
258
- → Restarts terminal with same config
259
-
260
- kill_terminal(id)
261
- → Graceful shutdown (SIGINT → SIGTERM → SIGKILL)
262
-
263
- set_label(id, label)
264
- → Rename terminal
265
- ```
266
-
267
- ### Session Tools
268
- ```
269
- get_session_info()
270
- → {tier, terminalsMax, features, terminals, createdAt}
271
-
272
- finalize_session()
273
- → Triggers post-session: tool rating, hypothesis validation, playbook evolution
274
- ```
275
-
276
- ---
277
-
278
- ## Startup Sequence
279
-
280
- 1. `list_terminals` — check all terminals alive
281
- 2. If any down → `restart_terminal(id)`
282
- 3. Decompose goal → criteria, paths, milestones, assignments
283
- 4. Present plan (3-5 bullets), get approval
284
- 5. Begin dispatching via `assign_task` or `send_input`
285
- 6. Start monitoring loop: MCP tools every 30-60s + occasional screenshots
286
-
287
- ---
288
-
289
- ## Safety
290
-
291
- - Do NOT send money, make purchases, or create financial obligations without approval
292
- - Do NOT send messages to people without approval
293
- - Do NOT post public content without approval
294
- - When in doubt, ask. The cost of asking is low.
5
+ This file exists for backwards compatibility. Read the canonical file instead.
package/public/app.js CHANGED
@@ -47,6 +47,31 @@ const auth = {
47
47
  }
48
48
  },
49
49
 
50
+ async tryBootstrap() {
51
+ try {
52
+ const res = await fetch(`${API_BASE}/api/auth/bootstrap`);
53
+ if (!res.ok) return false;
54
+
55
+ const data = await res.json();
56
+ if (!data.token) return false;
57
+
58
+ // Validate token format
59
+ const parts = data.token.split('.');
60
+ if (parts.length !== 3) return false;
61
+
62
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
63
+ if (payload.exp && payload.exp * 1000 < Date.now()) return false;
64
+
65
+ // Save to localStorage and use
66
+ localStorage.setItem(TOKEN_KEY, data.token);
67
+ this.token = data.token;
68
+ this.user = payload.sub || payload.email || payload.username || null;
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ },
74
+
50
75
  async login(usernameOrEmail, password) {
51
76
  const res = await fetch(`${AUTH_API}/auth/login`, {
52
77
  method: 'POST',
@@ -317,6 +342,7 @@ const STATE_ICONS = {
317
342
  waiting_approval: '\u26A0', // warning
318
343
  starting: '\u25CB',
319
344
  exited: '\u2717',
345
+ stuck: '\u26A0',
320
346
  };
321
347
 
322
348
  const STATE_LABELS = {
@@ -329,6 +355,26 @@ const STATE_LABELS = {
329
355
  waiting_approval: 'APPROVAL',
330
356
  starting: 'STARTING',
331
357
  exited: 'EXITED',
358
+ stuck: 'STUCK',
359
+ };
360
+
361
+ // Task status (semantic, separate from process status)
362
+ const TASK_STATUS_ICONS = {
363
+ pending: '\u25CB', // ○
364
+ running: '\u25B7', // ▷
365
+ done: '\u2713', // ✓
366
+ blocked: '\u26A0', // ⚠
367
+ error: '\u2717', // ✗
368
+ unknown: '?',
369
+ };
370
+
371
+ const TASK_STATUS_LABELS = {
372
+ pending: 'PENDING',
373
+ running: 'RUNNING',
374
+ done: 'DONE',
375
+ blocked: 'BLOCKED',
376
+ error: 'ERROR',
377
+ unknown: 'UNKNOWN',
332
378
  };
333
379
 
334
380
  // ── Utilities ────────────────────────────────────────────────
@@ -399,7 +445,7 @@ function createTerminalUI(termData) {
399
445
  startLabelEdit(id, labelEl);
400
446
  });
401
447
 
402
- // State indicator
448
+ // State indicator (process status)
403
449
  const stateEl = document.createElement('span');
404
450
  stateEl.className = 'pane-state';
405
451
  const stateIcon = document.createElement('span');
@@ -412,6 +458,23 @@ function createTerminalUI(termData) {
412
458
  stateEl.appendChild(stateIcon);
413
459
  stateEl.appendChild(stateText);
414
460
 
461
+ // Task status indicator (semantic status)
462
+ const taskStatusEl = document.createElement('span');
463
+ taskStatusEl.className = 'pane-task-status';
464
+ taskStatusEl.id = `task-status-${id}`;
465
+ const taskState = termData.taskStatus || 'pending';
466
+ const taskIcon = document.createElement('span');
467
+ taskIcon.className = `task-icon ${taskState}`;
468
+ taskIcon.id = `task-icon-${id}`;
469
+ taskIcon.textContent = TASK_STATUS_ICONS[taskState] || '?';
470
+ const taskText = document.createElement('span');
471
+ taskText.className = `task-text ${taskState}`;
472
+ taskText.id = `task-text-${id}`;
473
+ taskText.textContent = TASK_STATUS_LABELS[taskState] || 'PENDING';
474
+ taskStatusEl.appendChild(taskIcon);
475
+ taskStatusEl.appendChild(taskText);
476
+ taskStatusEl.title = termData.taskStatusMessage || '';
477
+
415
478
  // Elapsed
416
479
  const elapsedEl = document.createElement('span');
417
480
  elapsedEl.className = 'pane-elapsed';
@@ -449,6 +512,7 @@ function createTerminalUI(termData) {
449
512
 
450
513
  header.appendChild(labelEl);
451
514
  header.appendChild(stateEl);
515
+ header.appendChild(taskStatusEl);
452
516
  header.appendChild(elapsedEl);
453
517
  header.appendChild(spacer);
454
518
  header.appendChild(actionsEl);
@@ -525,6 +589,68 @@ function createTerminalUI(termData) {
525
589
  if (ws.readyState === WebSocket.OPEN) ws.send(data);
526
590
  });
527
591
 
592
+ // Auto-press Enter after paste (for orchestrator browser automation)
593
+ // Only triggers on actual paste events, not fast typing
594
+ container.addEventListener('paste', () => {
595
+ setTimeout(() => {
596
+ if (ws.readyState === WebSocket.OPEN) {
597
+ ws.send('\r');
598
+ }
599
+ }, 150);
600
+ });
601
+
602
+ // ── Image Drag & Drop ─────────────────────────────────────────
603
+ // Prevent browser default (opening file in new tab)
604
+ pane.addEventListener('dragover', (e) => {
605
+ e.preventDefault();
606
+ e.stopPropagation();
607
+ pane.classList.add('drag-over');
608
+ });
609
+
610
+ pane.addEventListener('dragleave', (e) => {
611
+ e.preventDefault();
612
+ e.stopPropagation();
613
+ pane.classList.remove('drag-over');
614
+ });
615
+
616
+ pane.addEventListener('drop', async (e) => {
617
+ e.preventDefault();
618
+ e.stopPropagation();
619
+ pane.classList.remove('drag-over');
620
+
621
+ const files = e.dataTransfer?.files;
622
+ if (!files || files.length === 0) return;
623
+
624
+ const file = files[0];
625
+ term.write(`\r\n\x1b[36m[Uploading ${file.name}...]\x1b[0m`);
626
+
627
+ try {
628
+ const formData = new FormData();
629
+ formData.append('file', file);
630
+
631
+ const res = await fetch('/api/upload', {
632
+ method: 'POST',
633
+ headers: auth.token ? { 'Authorization': `Bearer ${auth.token}` } : {},
634
+ body: formData,
635
+ });
636
+
637
+ if (!res.ok) {
638
+ const err = await res.json().catch(() => ({}));
639
+ throw new Error(err.error || `Upload failed: ${res.status}`);
640
+ }
641
+
642
+ const data = await res.json();
643
+ term.write(`\r\n\x1b[32m[File saved: ${data.path}]\x1b[0m\r\n`);
644
+
645
+ // Inject the path reference into the terminal for Claude to use
646
+ if (ws.readyState === WebSocket.OPEN) {
647
+ ws.send(`[File uploaded: ${data.path}]\r`);
648
+ }
649
+ } catch (err) {
650
+ term.write(`\r\n\x1b[31m[Upload failed: ${err.message}]\x1b[0m\r\n`);
651
+ }
652
+ });
653
+
528
654
  term.onResize(({ cols, rows }) => {
529
655
  if (ws.readyState === WebSocket.OPEN) {
530
656
  ws.send(JSON.stringify({ type: 'resize', cols, rows }));
@@ -790,6 +916,40 @@ function fitAll() {
790
916
 
791
917
  // ── Status Updates ───────────────────────────────────────────
792
918
 
919
+ function updateTaskStatus(id, taskState, message) {
920
+ const t = state.terminals.get(id);
921
+ if (!t) return;
922
+
923
+ t.taskStatus = taskState;
924
+ t.taskStatusMessage = message || '';
925
+
926
+ // Update task icon
927
+ const taskIcon = document.getElementById(`task-icon-${id}`);
928
+ if (taskIcon) {
929
+ taskIcon.className = `task-icon ${taskState}`;
930
+ taskIcon.textContent = TASK_STATUS_ICONS[taskState] || '?';
931
+ }
932
+
933
+ // Update task text
934
+ const taskText = document.getElementById(`task-text-${id}`);
935
+ if (taskText) {
936
+ taskText.className = `task-text ${taskState}`;
937
+ taskText.textContent = TASK_STATUS_LABELS[taskState] || taskState.toUpperCase();
938
+ }
939
+
940
+ // Update tooltip
941
+ const taskStatusEl = document.getElementById(`task-status-${id}`);
942
+ if (taskStatusEl) {
943
+ taskStatusEl.title = message || '';
944
+ }
945
+
946
+ // Update pane border based on task status (prioritize task status for orchestration)
947
+ if (t.paneEl) {
948
+ t.paneEl.classList.remove('task-done', 'task-blocked', 'task-error', 'task-running');
949
+ t.paneEl.classList.add(`task-${taskState}`);
950
+ }
951
+ }
952
+
793
953
  function updateTerminalState(id, newStatus, extra) {
794
954
  const t = state.terminals.get(id);
795
955
  if (!t) return;
@@ -934,6 +1094,21 @@ function connectSSE() {
934
1094
  } catch {}
935
1095
  });
936
1096
 
1097
+ evtSource.addEventListener('task_status_change', (e) => {
1098
+ try {
1099
+ const data = JSON.parse(e.data);
1100
+ const { id, taskStatus } = data;
1101
+ if (taskStatus && taskStatus.state) {
1102
+ updateTaskStatus(id, taskStatus.state, taskStatus.message);
1103
+
1104
+ const t = state.terminals.get(id);
1105
+ const name = t ? t.label : `T${id}`;
1106
+ const stateLabel = TASK_STATUS_LABELS[taskStatus.state] || taskStatus.state;
1107
+ addFeedEntry(`${name} TASK: ${stateLabel}${taskStatus.message ? ` — ${taskStatus.message.slice(0, 50)}` : ''}`, id);
1108
+ }
1109
+ } catch {}
1110
+ });
1111
+
937
1112
  evtSource.addEventListener('progress', (e) => {
938
1113
  try {
939
1114
  const data = JSON.parse(e.data);
@@ -1026,6 +1201,11 @@ async function pollStatus() {
1026
1201
  taskName: item.taskName,
1027
1202
  });
1028
1203
  }
1204
+
1205
+ // Update task status if changed (SSE is primary, this is fallback)
1206
+ if (item.taskStatus && item.taskStatus !== t.taskStatus) {
1207
+ updateTaskStatus(item.id, item.taskStatus, item.taskStatusMessage);
1208
+ }
1029
1209
  }
1030
1210
 
1031
1211
  // Check for removed terminals
@@ -1247,9 +1427,22 @@ async function init() {
1247
1427
  sessionReadyResolve();
1248
1428
  });
1249
1429
  } else {
1250
- // No valid local token — show login
1251
- showAuthOverlay();
1252
- sessionReadyResolve(); // Unblock any waiting code
1430
+ // No valid local token — try bootstrap from saved server token
1431
+ const bootstrapped = await auth.tryBootstrap();
1432
+ if (bootstrapped) {
1433
+ hideAuthOverlay();
1434
+ startApp();
1435
+ auth.validateTier()
1436
+ .then(result => {
1437
+ if (result?.needsLogin) showAuthOverlay();
1438
+ })
1439
+ .catch(err => console.warn('Tier validation failed:', err))
1440
+ .finally(() => sessionReadyResolve());
1441
+ } else {
1442
+ // No saved token — show login
1443
+ showAuthOverlay();
1444
+ sessionReadyResolve();
1445
+ }
1253
1446
  }
1254
1447
  }
1255
1448