runtime-inspector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Asaf Erdman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,278 @@
1
+ <p align="center">
2
+ <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="Node.js 18+" />
3
+ <img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue" alt="macOS | Linux" />
4
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" />
5
+ <img src="https://img.shields.io/badge/status-coming%20soon-orange" alt="Coming Soon" />
6
+ </p>
7
+
8
+ <h1 align="center">RuntimeInspector</h1>
9
+
10
+ <p align="center">
11
+ <strong>See what your AI agents are really doing.</strong><br/>
12
+ A local CLI tool that scans your processes, groups them into sessions,<br/>
13
+ and serves a real-time dashboard. Install from npm. Runs on localhost. No cloud.
14
+ </p>
15
+
16
+ <br/>
17
+
18
+ <p align="center">
19
+ <img width="720" src="https://github.com/user-attachments/assets/placeholder-dashboard.png" alt="RuntimeInspector Dashboard" />
20
+ </p>
21
+
22
+ > **Note:** `runtime-inspector` is not yet published to npm. Star this repo to get notified when it drops.
23
+
24
+ ---
25
+
26
+ ## Why
27
+
28
+ Modern dev workflows spawn dozens of invisible processes — AI agents, dev servers, Docker containers, test watchers. Without visibility, you're flying blind.
29
+
30
+ ```
31
+ $ ps aux | grep node
32
+ # 47 lines of mystery
33
+ ```
34
+
35
+ RuntimeInspector replaces that chaos with a single, organized dashboard:
36
+
37
+ ```
38
+ ┌─────────────────────────────────────────────────────────┐
39
+ │ RuntimeInspector · localhost:7331 6 sessions │
40
+ ├─────────────────────────────────────────────────────────┤
41
+ │ │
42
+ │ ● Claude Code — Refactor auth acme/web-app │
43
+ │ PID 48201 + 3 children · 18m · CPU 12% · 340 MB │
44
+ │ Spawned 3 child shells; editing 12 files in │
45
+ │ src/auth/. Dev server on :3000. │
46
+ │ │
47
+ │ ● Codex — Generate API tests acme/api-service │
48
+ │ PID 48305 + 8 children · 42m · CPU 68% · 1.2 GB │
49
+ │ ⚠ HIGH CPU — Jest watch mode, 23 suites in parallel │
50
+ │ │
51
+ │ ● Vite Dev Server acme/dashboard-ui │
52
+ │ PID 47102 · 2h 14m · CPU 3% · 180 MB │
53
+ │ ⚠ LONG-RUNNING — No file changes in 47 min │
54
+ │ │
55
+ │ ● Express API — payments acme/payments │
56
+ │ PID 44089 · 3h 02m · CPU 1% · 95 MB │
57
+ │ ⚠ ORPHAN — Parent gone. Still bound to :4000. │
58
+ │ │
59
+ │ ● Docker — Postgres + Redis acme/infra │
60
+ │ PID 43998 + 2 containers · 4h 50m · 520 MB │
61
+ │ postgres:16 on :5432, redis:7 on :6379. Healthy. │
62
+ │ │
63
+ │ ● Cursor Agent — Fix CI pipeline acme/deploy-config │
64
+ │ PID 49120 · 7m · CPU 22% · 410 MB │
65
+ │ Reading .github/workflows/. Modifying deploy.yml. │
66
+ │ │
67
+ └─────────────────────────────────────────────────────────┘
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Quick Start
73
+
74
+ ### 1. Install globally
75
+
76
+ ```bash
77
+ npm install -g runtime-inspector
78
+ ```
79
+
80
+ ### 2. Add shell integration (optional)
81
+
82
+ ```bash
83
+ runtime-inspector setup --yes
84
+ ```
85
+
86
+ This installs lightweight `preexec`/`precmd` hooks into your shell (zsh, bash, or fish) for command-level tracking. Restart your terminal after setup.
87
+
88
+ ### 3. Start the dashboard
89
+
90
+ ```bash
91
+ runtime-inspector start --open
92
+ ```
93
+
94
+ Your browser opens `http://localhost:7331` with a live view of everything running on your machine — grouped, explained, and scored.
95
+
96
+ ### Bonus: wrap a command
97
+
98
+ ```bash
99
+ runtime-inspector run -- npm run dev
100
+ ```
101
+
102
+ Wraps any command for deeper output tracking and richer session metadata.
103
+
104
+ ---
105
+
106
+ ## How It Works
107
+
108
+ ```
109
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
110
+ │ 1. SCAN │────▶│ 2. GROUP │────▶│ 3. EXPLAIN │
111
+ │ │ │ │ │ │
112
+ │ Read process │ │ Cluster by │ │ Plain-English│
113
+ │ tree: PIDs, │ │ agent, repo, │ │ summaries + │
114
+ │ commands, │ │ runtime into │ │ risk signals │
115
+ │ ports, CPU │ │ sessions │ │ per session │
116
+ └──────────────┘ └──────────────┘ └──────────────┘
117
+ ```
118
+
119
+ **Scan** — Reads your process tree to identify CLIs, agents, servers, and their children.
120
+
121
+ **Group** — Clusters related processes by agent, repository, and runtime into coherent sessions.
122
+
123
+ **Explain** — Each session gets a summary and risk signals: long-running, orphaned, duplicated, or resource-heavy.
124
+
125
+ ---
126
+
127
+ ## CLI Reference
128
+
129
+ ```
130
+ runtime-inspector start [--open] Start agent + dashboard
131
+ runtime-inspector run -- <command> Run a command with output tracking
132
+ runtime-inspector init [--shell zsh] Print shell hook script to stdout
133
+ runtime-inspector setup [--yes] Install shell integration into rc file
134
+ runtime-inspector --help Show help
135
+ ```
136
+
137
+ ### Setup options
138
+
139
+ | Flag | Description |
140
+ |---|---|
141
+ | `--shell zsh\|bash` | Override shell detection |
142
+ | `--rc <path>` | Override rc file path |
143
+ | `--yes` | Skip confirmation prompt |
144
+ | `--open`, `-o` | Open browser after starting |
145
+
146
+ ---
147
+
148
+ ## What Gets Detected
149
+
150
+ ### AI Agents & Coding Tools
151
+ | Tool | Status |
152
+ |---|---|
153
+ | Claude Code | Supported |
154
+ | OpenAI Codex CLI | Supported |
155
+ | Cursor Agent | Supported |
156
+ | Aider | Supported |
157
+ | GitHub Copilot CLI | Supported |
158
+ | Continue | Supported |
159
+
160
+ ### Frameworks & Runtimes
161
+ | Runtime | Status |
162
+ |---|---|
163
+ | Node.js | Supported |
164
+ | Vite / Next.js | Supported |
165
+ | Docker | Supported |
166
+ | Python | Supported |
167
+ | Go | Supported |
168
+ | Rust (cargo) | Supported |
169
+
170
+ Detection is plugin-based — adding new tools takes minutes.
171
+
172
+ ---
173
+
174
+ ## Risk Signals
175
+
176
+ RuntimeInspector automatically flags sessions that need attention:
177
+
178
+ | Signal | Trigger | Default |
179
+ |---|---|---|
180
+ | **Long-running** | Session exceeds duration threshold | 30 min |
181
+ | **High CPU** | CPU usage exceeds threshold | 80% |
182
+ | **Orphan** | Parent process no longer exists | Immediate |
183
+ | **Duplicate** | Multiple instances of the same agent/server | Immediate |
184
+
185
+ All thresholds are configurable via YAML config.
186
+
187
+ ---
188
+
189
+ ## Privacy & Security
190
+
191
+ RuntimeInspector is **local-first by design**:
192
+
193
+ - Runs entirely on `localhost` — no cloud dependency
194
+ - Reads **process metadata only** (PIDs, command names, ports, resource counters)
195
+ - **No source code** or terminal output leaves your machine
196
+ - No telemetry, no tracking, no accounts
197
+ - Configurable allowlist for any outbound data
198
+
199
+ ---
200
+
201
+ ## Project Structure
202
+
203
+ ```
204
+ runtime-inspector/
205
+ ├── bin/
206
+ │ └── cli.js # CLI entry point
207
+ ├── src/
208
+ │ ├── agent/
209
+ │ │ ├── index.js # Express server + dashboard
210
+ │ │ ├── scanner.js # Process tree scanner
211
+ │ │ ├── detector.js # Agent/framework detection
212
+ │ │ ├── grouper.js # Session grouping logic
213
+ │ │ ├── explainer.js # Plain-English summaries
214
+ │ │ ├── purposer.js # Intent inference
215
+ │ │ ├── stateInfer.js # State detection
216
+ │ │ ├── progressInfer.js # Progress tracking
217
+ │ │ ├── attentionInfer.js# Attention signals
218
+ │ │ ├── actions.js # Suggested actions
219
+ │ │ ├── dashboard.js # Dashboard HTML renderer
220
+ │ │ ├── shellEvents.js # Shell event processing
221
+ │ │ ├── shellMerge.js # Shell session merging
222
+ │ │ ├── repoActivity.js # Repo activity tracking
223
+ │ │ ├── tmux.js # Tmux detection
224
+ │ │ └── tmuxMerge.js # Tmux session merging
225
+ │ ├── shell/
226
+ │ │ ├── hooks.js # Shell hook script generator
227
+ │ │ └── setup.js # RC file installer
228
+ │ └── wrapper/
229
+ │ ├── runner.js # Command wrapper
230
+ │ └── buffer.js # Output buffer
231
+ ├── runtimeinspector/ # Next.js landing page
232
+ │ └── ...
233
+ ├── package.json
234
+ └── README.md
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Requirements
240
+
241
+ - **Node.js 18** or later
242
+ - **macOS** or **Linux** (Windows/WSL on the roadmap)
243
+
244
+ ---
245
+
246
+ ## Contributing
247
+
248
+ Contributions are welcome! Feel free to open issues or submit PRs.
249
+
250
+ ```bash
251
+ # Clone the repo
252
+ git clone https://github.com/asaferdman23/runtime_inspector_landing.git
253
+ cd runtime_inspector_landing
254
+
255
+ # Install CLI dependencies
256
+ npm install
257
+
258
+ # Install landing page dependencies
259
+ cd runtimeinspector && npm install
260
+
261
+ # Run the CLI
262
+ node bin/cli.js start --open
263
+
264
+ # Run the landing page
265
+ cd runtimeinspector && npm run dev
266
+ ```
267
+
268
+ ---
269
+
270
+ ## License
271
+
272
+ [MIT](LICENSE) — Asaf Erdman
273
+
274
+ ---
275
+
276
+ <p align="center">
277
+ <strong>Star this repo</strong> to get notified when <code>runtime-inspector</code> lands on npm.
278
+ </p>
package/bin/cli.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+
3
+ // RuntimeInspector CLI
4
+ // Usage: runtime-inspector start [--open]
5
+ // runtime-inspector run -- <command> [args...]
6
+ // runtime-inspector init [--shell zsh|bash]
7
+ // runtime-inspector setup [--shell zsh|bash] [--rc <path>] [--yes]
8
+
9
+ const args = process.argv.slice(2);
10
+ const command = args[0];
11
+
12
+ function getFlag(name) {
13
+ const idx = args.indexOf(name);
14
+ if (idx === -1) return null;
15
+ return args[idx + 1] || null;
16
+ }
17
+
18
+ function hasFlag(name) {
19
+ return args.includes(name);
20
+ }
21
+
22
+ if (command === 'init') {
23
+ // Print shell hook script to stdout
24
+ const shell = getFlag('--shell') || 'zsh';
25
+ import('../src/shell/hooks.js').then(({ generateHookScript }) => {
26
+ process.stdout.write(generateHookScript(shell));
27
+ }).catch((err) => {
28
+ console.error('Failed to generate hook script:', err.message);
29
+ process.exit(1);
30
+ });
31
+
32
+ } else if (command === 'setup') {
33
+ // Install integration into shell rc file
34
+ const shell = getFlag('--shell') || null;
35
+ const rc = getFlag('--rc') || null;
36
+ const yes = hasFlag('--yes');
37
+
38
+ import('../src/shell/setup.js').then(({ detectShell, installRcBlock }) => {
39
+ const { shell: detectedShell, rcPath } = detectShell(shell, rc);
40
+
41
+ if (!yes) {
42
+ console.log(` RuntimeInspector Shell Integration`);
43
+ console.log(` Shell: ${detectedShell}`);
44
+ console.log(` RC file: ${rcPath}`);
45
+ console.log('');
46
+ console.log(' This will append the RuntimeInspector integration block to your rc file.');
47
+ console.log(' A backup will be created before any changes.');
48
+ console.log('');
49
+ console.log(' Run with --yes to proceed, or:');
50
+ console.log(` runtimeinspector setup --shell ${detectedShell} --yes`);
51
+ return;
52
+ }
53
+
54
+ const result = installRcBlock({ shell: detectedShell, rcPath, yes });
55
+
56
+ if (result.status === 'already') {
57
+ console.log(` ${result.message}`);
58
+ console.log(' No changes made.');
59
+ } else if (result.status === 'installed') {
60
+ console.log(` ${result.message}`);
61
+ console.log('');
62
+ console.log(' Next steps:');
63
+ console.log(' 1. Restart your terminal (or run: source ' + rcPath + ')');
64
+ console.log(' 2. Start the agent: runtimeinspector start');
65
+ console.log(' 3. Open dashboard: http://localhost:7331');
66
+ } else {
67
+ console.error(` Error: ${result.message}`);
68
+ process.exit(1);
69
+ }
70
+ }).catch((err) => {
71
+ console.error('Failed to run setup:', err.message);
72
+ process.exit(1);
73
+ });
74
+
75
+ } else if (command === 'run') {
76
+ // Find the -- separator
77
+ const dashIndex = args.indexOf('--');
78
+ if (dashIndex === -1 || dashIndex === args.length - 1) {
79
+ console.error('Usage: runtime-inspector run -- <command> [args...]');
80
+ console.error('Example: runtime-inspector run -- npm run dev');
81
+ process.exit(1);
82
+ }
83
+ const cmdArgs = args.slice(dashIndex + 1);
84
+
85
+ import('../src/wrapper/runner.js').then(({ runWrapped }) => {
86
+ runWrapped(cmdArgs);
87
+ }).catch((err) => {
88
+ console.error('Failed to start wrapper:', err.message);
89
+ process.exit(1);
90
+ });
91
+
92
+ } else if (!command || command === 'start') {
93
+ const shouldOpen = args.includes('--open') || args.includes('-o');
94
+
95
+ import('../src/agent/index.js').then(({ startServer }) => {
96
+ startServer({ open: shouldOpen });
97
+ }).catch((err) => {
98
+ console.error('Failed to start RuntimeInspector:', err.message);
99
+ process.exit(1);
100
+ });
101
+
102
+ } else if (command === '--help' || command === '-h') {
103
+ console.log(`
104
+ RuntimeInspector — local runtime inspection tool
105
+
106
+ Usage:
107
+ runtime-inspector start [--open] Start the agent + dashboard
108
+ runtime-inspector run -- <command> Run a command through RuntimeInspector
109
+ runtime-inspector init [--shell zsh] Print shell hook script to stdout
110
+ runtime-inspector setup [--yes] Install shell integration into rc file
111
+ runtime-inspector --help Show this help
112
+
113
+ Setup options:
114
+ --shell zsh|bash Override shell detection
115
+ --rc <path> Override rc file path
116
+ --yes Skip confirmation prompt
117
+
118
+ Quick start:
119
+ runtimeinspector setup --yes
120
+ # restart terminal
121
+ runtimeinspector start --open
122
+
123
+ Dashboard: http://localhost:7331
124
+ `);
125
+ } else {
126
+ console.error(`Unknown command: ${command}`);
127
+ console.error('Run "runtime-inspector --help" for usage.');
128
+ process.exit(1);
129
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "runtime-inspector",
3
+ "version": "0.1.0",
4
+ "description": "Local-first runtime inspection tool for developers. Scans processes, groups them into sessions, and serves a real-time dashboard on localhost.",
5
+ "type": "module",
6
+ "bin": {
7
+ "runtime-inspector": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/cli.js start"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/agent/",
15
+ "src/shell/",
16
+ "src/wrapper/",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "cli",
22
+ "process",
23
+ "inspector",
24
+ "developer-tools",
25
+ "terminal",
26
+ "runtime",
27
+ "observability",
28
+ "ai-agents",
29
+ "dashboard",
30
+ "localhost"
31
+ ],
32
+ "author": "Asaf Erdman <asaf.erdman.dev@gmail.com>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/asaferdman23/runtime_inspector_landing.git"
37
+ },
38
+ "homepage": "https://github.com/asaferdman23/runtime_inspector_landing#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/asaferdman23/runtime_inspector_landing/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "express": "^4.21.0",
47
+ "open": "^10.1.0"
48
+ }
49
+ }
@@ -0,0 +1,157 @@
1
+ // Context actions — safe OS-level actions triggered from the dashboard.
2
+ // Only whitelisted actions. No arbitrary command execution.
3
+
4
+ import { execSync, spawnSync } from 'child_process';
5
+
6
+ /**
7
+ * Open a terminal window at the given directory path.
8
+ * macOS: uses osascript to open Terminal.app.
9
+ */
10
+ function openTerminal(cwd) {
11
+ if (!cwd || typeof cwd !== 'string') throw new Error('No working directory available');
12
+ if (!cwd.startsWith('/')) throw new Error('Invalid path');
13
+
14
+ // AppleScript to open Terminal.app at a specific directory
15
+ const script = `tell application "Terminal"
16
+ do script "cd ${escapeAppleScript(cwd)}"
17
+ activate
18
+ end tell`;
19
+
20
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { timeout: 5000 });
21
+ }
22
+
23
+ /**
24
+ * Open a folder in Finder (macOS file explorer).
25
+ */
26
+ function openFolder(cwd) {
27
+ if (!cwd || typeof cwd !== 'string') throw new Error('No working directory available');
28
+ if (!cwd.startsWith('/')) throw new Error('Invalid path');
29
+
30
+ execSync(`open "${cwd}"`, { timeout: 5000 });
31
+ }
32
+
33
+ /**
34
+ * Escape a string for AppleScript.
35
+ */
36
+ function escapeAppleScript(str) {
37
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
38
+ }
39
+
40
+ /**
41
+ * Register action routes on the Express app.
42
+ * Requires a function to look up sessions by id.
43
+ */
44
+ export function registerActions(app, getSession) {
45
+ app.use('/api/actions', (req, res, next) => {
46
+ // All action routes use JSON
47
+ if (req.method === 'POST') {
48
+ let body = '';
49
+ req.on('data', chunk => body += chunk);
50
+ req.on('end', () => {
51
+ try { req.body = JSON.parse(body); } catch { req.body = {}; }
52
+ next();
53
+ });
54
+ } else {
55
+ next();
56
+ }
57
+ });
58
+
59
+ // POST /api/actions/open-terminal { sessionId }
60
+ app.post('/api/actions/open-terminal', (req, res) => {
61
+ try {
62
+ const session = getSession(req.body?.sessionId);
63
+ if (!session) return res.status(404).json({ error: 'Session not found' });
64
+ if (!session.cwd) return res.status(400).json({ error: 'No working directory for this session' });
65
+
66
+ openTerminal(session.cwd);
67
+ res.json({ ok: true, action: 'open-terminal', cwd: session.cwd });
68
+ } catch (err) {
69
+ res.status(500).json({ error: err.message });
70
+ }
71
+ });
72
+
73
+ // POST /api/actions/open-folder { sessionId }
74
+ app.post('/api/actions/open-folder', (req, res) => {
75
+ try {
76
+ const session = getSession(req.body?.sessionId);
77
+ if (!session) return res.status(404).json({ error: 'Session not found' });
78
+ if (!session.cwd) return res.status(400).json({ error: 'No working directory for this session' });
79
+
80
+ openFolder(session.cwd);
81
+ res.json({ ok: true, action: 'open-folder', cwd: session.cwd });
82
+ } catch (err) {
83
+ res.status(500).json({ error: err.message });
84
+ }
85
+ });
86
+
87
+ // POST /api/actions/focus-terminal { sessionId }
88
+ // Best-effort: activate Terminal.app and switch to the TTY tab
89
+ app.post('/api/actions/focus-terminal', (req, res) => {
90
+ try {
91
+ const session = getSession(req.body?.sessionId);
92
+ if (!session) return res.status(404).json({ error: 'Session not found' });
93
+
94
+ const cwd = session.cwd;
95
+ if (!cwd) return res.status(400).json({ error: 'No working directory for this session' });
96
+
97
+ // Best-effort: open Terminal.app at cwd. If the session has a tty, try to find that tab.
98
+ const script = `tell application "Terminal"
99
+ activate
100
+ do script "cd ${escapeAppleScript(cwd)}"
101
+ end tell`;
102
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { timeout: 5000 });
103
+ res.json({ ok: true, action: 'focus-terminal', cwd });
104
+ } catch (err) {
105
+ res.status(500).json({ error: err.message });
106
+ }
107
+ });
108
+
109
+ // POST /api/actions/tmux-focus { sessionId }
110
+ // Switch tmux client to the pane associated with this session
111
+ app.post('/api/actions/tmux-focus', (req, res) => {
112
+ try {
113
+ const session = getSession(req.body?.sessionId);
114
+ if (!session) return res.status(404).json({ error: 'Session not found' });
115
+ if (!session.tmux) return res.status(400).json({ error: 'Session has no tmux pane' });
116
+
117
+ const { paneId, sessionName } = session.tmux;
118
+
119
+ // Try: switch-client to session and select-pane
120
+ const r = spawnSync('tmux', ['switch-client', '-t', sessionName, ';', 'select-pane', '-t', paneId], {
121
+ timeout: 2000,
122
+ encoding: 'utf-8',
123
+ stdio: 'pipe',
124
+ });
125
+
126
+ if (r.status !== 0) {
127
+ // Fallback: just select-pane (works if already in that session)
128
+ const r2 = spawnSync('tmux', ['select-pane', '-t', paneId], {
129
+ timeout: 2000,
130
+ encoding: 'utf-8',
131
+ stdio: 'pipe',
132
+ });
133
+ if (r2.status !== 0) {
134
+ return res.json({ ok: false, error: 'Could not focus tmux pane — are you in tmux?' });
135
+ }
136
+ }
137
+
138
+ res.json({ ok: true, action: 'tmux-focus', paneId });
139
+ } catch (err) {
140
+ res.status(500).json({ error: err.message });
141
+ }
142
+ });
143
+
144
+ // POST /api/actions/copy-command { sessionId }
145
+ // Returns the root process command so the frontend can copy it.
146
+ app.post('/api/actions/copy-command', (req, res) => {
147
+ try {
148
+ const session = getSession(req.body?.sessionId);
149
+ if (!session) return res.status(404).json({ error: 'Session not found' });
150
+
151
+ const rootCmd = session.processes[0]?.cmd || '';
152
+ res.json({ ok: true, action: 'copy-command', command: rootCmd });
153
+ } catch (err) {
154
+ res.status(500).json({ error: err.message });
155
+ }
156
+ });
157
+ }