tui-td 0.2.10 → 0.2.12

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,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Layout/LineLength
4
+
3
5
  require "json"
4
6
 
5
7
  module TUITD
@@ -35,7 +37,7 @@ module TUITD
35
37
  $stderr.sync = true
36
38
 
37
39
  # Signal readiness
38
- $stderr.puts "[tui-td MCP] Server started, awaiting JSON-RPC on stdin..."
40
+ warn "[tui-td MCP] Server started, awaiting JSON-RPC on stdin..."
39
41
 
40
42
  while @running && (line = $stdin.gets)
41
43
  line = line.strip
@@ -48,16 +50,18 @@ module TUITD
48
50
  puts JSON.generate(response) if response
49
51
  $stdout.flush
50
52
  rescue JSON::ParserError => e
51
- error_response(nil, -32700, "Parse error: #{e.message}")
53
+ error_response(nil, -32_700, "Parse error: #{e.message}")
52
54
  rescue StandardError => e
53
- $stderr.puts "[tui-td MCP] Error: #{e.class}: #{e.message}"
54
- $stderr.puts e.backtrace.first(5).join("\n ") if $DEBUG
55
+ warn "[tui-td MCP] Error: #{e.class}: #{e.message}"
56
+ warn e.backtrace.first(5).join("\n ") if $DEBUG
55
57
  end
56
58
  end
57
59
 
58
60
  @driver&.close
59
61
  end
60
62
 
63
+ ALLOWED_OUTPUT_DIRS = ["/tmp"].freeze
64
+
61
65
  private
62
66
 
63
67
  def handle_request(request)
@@ -69,35 +73,35 @@ module TUITD
69
73
  when "initialize"
70
74
  handle_initialize(params, id)
71
75
  when "notifications/initialized"
72
- nil # No response needed
76
+ nil # No response needed
73
77
  when "tools/list"
74
78
  handle_tools_list(id)
75
79
  when "tools/call"
76
80
  handle_tools_call(params, id)
77
81
  else
78
82
  if method&.start_with?("notifications/")
79
- nil # Ignore unknown notifications
83
+ nil # Ignore unknown notifications
80
84
  else
81
- error_response(id, -32601, "Method not found: #{method}")
85
+ error_response(id, -32_601, "Method not found: #{method}")
82
86
  end
83
87
  end
84
88
  end
85
89
 
86
90
  # Initialize handshake
87
- def handle_initialize(params, id)
91
+ def handle_initialize(_params, id)
88
92
  {
89
93
  jsonrpc: "2.0",
90
94
  id: id,
91
95
  result: {
92
96
  protocolVersion: PROTOCOL_VERSION,
93
97
  capabilities: {
94
- tools: {}
98
+ tools: {},
95
99
  },
96
100
  serverInfo: {
97
101
  name: SERVER_NAME,
98
- version: SERVER_VERSION
99
- }
100
- }
102
+ version: SERVER_VERSION,
103
+ },
104
+ },
101
105
  }
102
106
  end
103
107
 
@@ -116,26 +120,26 @@ module TUITD
116
120
  properties: {
117
121
  command: {
118
122
  type: "string",
119
- description: "The command to run (e.g., 'htop', 'vim file.txt')"
123
+ description: "The command to run (e.g., 'htop', 'vim file.txt')",
120
124
  },
121
125
  rows: {
122
126
  type: "integer",
123
127
  description: "Terminal height in rows (default: 40)",
124
- default: 40
128
+ default: 40,
125
129
  },
126
130
  cols: {
127
131
  type: "integer",
128
132
  description: "Terminal width in columns (default: 120)",
129
- default: 120
133
+ default: 120,
130
134
  },
131
135
  timeout: {
132
136
  type: "integer",
133
137
  description: "Timeout in seconds for waits (default: 30)",
134
- default: 30
135
- }
138
+ default: 30,
139
+ },
136
140
  },
137
- required: ["command"]
138
- }
141
+ required: ["command"],
142
+ },
139
143
  },
140
144
  {
141
145
  name: "tui_send",
@@ -145,11 +149,11 @@ module TUITD
145
149
  properties: {
146
150
  text: {
147
151
  type: "string",
148
- description: "Text to send to the TUI. Use \\n for enter/newline."
149
- }
152
+ description: "Text to send to the TUI. Use \\n for enter/newline.",
153
+ },
150
154
  },
151
- required: ["text"]
152
- }
155
+ required: ["text"],
156
+ },
153
157
  },
154
158
  {
155
159
  name: "tui_send_key",
@@ -159,14 +163,14 @@ module TUITD
159
163
  properties: {
160
164
  key: {
161
165
  type: "string",
162
- enum: ["enter", "tab", "escape", "up", "down", "left", "right",
163
- "backspace", "ctrl_c", "ctrl_d", "ctrl_z", "page_up", "page_down",
164
- "home", "end", "delete"],
165
- description: "Key to press"
166
- }
166
+ enum: %w[enter tab escape up down left right
167
+ backspace ctrl_c ctrl_d ctrl_z page_up page_down
168
+ home end delete],
169
+ description: "Key to press",
170
+ },
167
171
  },
168
- required: ["key"]
169
- }
172
+ required: ["key"],
173
+ },
170
174
  },
171
175
  {
172
176
  name: "tui_wait_for_text",
@@ -176,16 +180,16 @@ module TUITD
176
180
  properties: {
177
181
  text: {
178
182
  type: "string",
179
- description: "Text to wait for in the terminal output"
183
+ description: "Text to wait for in the terminal output",
180
184
  },
181
185
  timeout: {
182
186
  type: "integer",
183
187
  description: "Custom timeout in seconds (overrides default)",
184
- default: 30
185
- }
188
+ default: 30,
189
+ },
186
190
  },
187
- required: ["text"]
188
- }
191
+ required: ["text"],
192
+ },
189
193
  },
190
194
  {
191
195
  name: "tui_wait_for_stable",
@@ -196,10 +200,10 @@ module TUITD
196
200
  timeout: {
197
201
  type: "integer",
198
202
  description: "Custom timeout in seconds",
199
- default: 30
200
- }
201
- }
202
- }
203
+ default: 30,
204
+ },
205
+ },
206
+ },
203
207
  },
204
208
  {
205
209
  name: "tui_state",
@@ -209,20 +213,20 @@ module TUITD
209
213
  properties: {
210
214
  format: {
211
215
  type: "string",
212
- enum: ["ai", "full", "text"],
216
+ enum: %w[ai full text],
213
217
  description: "Output format: 'ai' (compact, text+highlights, default), 'full' (complete cell grid with ANSI colors), 'text' (plain text only)",
214
- default: "ai"
215
- }
216
- }
217
- }
218
+ default: "ai",
219
+ },
220
+ },
221
+ },
218
222
  },
219
223
  {
220
224
  name: "tui_plain_text",
221
225
  description: "Get the current terminal content as plain text (all ANSI stripped).",
222
226
  inputSchema: {
223
227
  type: "object",
224
- properties: {}
225
- }
228
+ properties: {},
229
+ },
226
230
  },
227
231
  {
228
232
  name: "tui_screenshot",
@@ -232,10 +236,10 @@ module TUITD
232
236
  properties: {
233
237
  path: {
234
238
  type: "string",
235
- description: "Output file path (optional, auto-generated if omitted)"
236
- }
237
- }
238
- }
239
+ description: "Output file path (optional, auto-generated if omitted)",
240
+ },
241
+ },
242
+ },
239
243
  },
240
244
  {
241
245
  name: "tui_html_render",
@@ -245,26 +249,26 @@ module TUITD
245
249
  properties: {
246
250
  path: {
247
251
  type: "string",
248
- description: "Optional file path to save the HTML. If omitted, the HTML content is returned inline so you can view it directly."
249
- }
250
- }
251
- }
252
+ description: "Optional file path to save the HTML. If omitted, the HTML content is returned inline so you can view it directly.",
253
+ },
254
+ },
255
+ },
252
256
  },
253
257
  {
254
258
  name: "tui_wait_for_exit",
255
259
  description: "Wait until the TUI process exits. Returns the exit status code (0 = success, non-zero = error).",
256
260
  inputSchema: {
257
261
  type: "object",
258
- properties: {}
259
- }
262
+ properties: {},
263
+ },
260
264
  },
261
265
  {
262
266
  name: "tui_exit_status",
263
267
  description: "Get the exit status of the TUI process. Returns nil if still running, otherwise the exit code.",
264
268
  inputSchema: {
265
269
  type: "object",
266
- properties: {}
267
- }
270
+ properties: {},
271
+ },
268
272
  },
269
273
  {
270
274
  name: "tui_find_text",
@@ -274,22 +278,22 @@ module TUITD
274
278
  properties: {
275
279
  pattern: {
276
280
  type: "string",
277
- description: "Text or regex pattern to search for (e.g., 'error', 'ERROR|FAIL')"
278
- }
281
+ description: "Text or regex pattern to search for (e.g., 'error', 'ERROR|FAIL')",
282
+ },
279
283
  },
280
- required: ["pattern"]
281
- }
284
+ required: ["pattern"],
285
+ },
282
286
  },
283
287
  {
284
288
  name: "tui_close",
285
289
  description: "Close the TUI application and clean up the PTY session. Call this when finished.",
286
290
  inputSchema: {
287
291
  type: "object",
288
- properties: {}
289
- }
290
- }
291
- ]
292
- }
292
+ properties: {},
293
+ },
294
+ },
295
+ ],
296
+ },
293
297
  }
294
298
  end
295
299
 
@@ -304,7 +308,7 @@ module TUITD
304
308
  when "tui_send_key" then call_tui_send_key(args)
305
309
  when "tui_wait_for_text" then call_tui_wait_for_text(args)
306
310
  when "tui_wait_for_stable" then call_tui_wait_for_stable(args)
307
- when "tui_state" then call_tui_state(args)
311
+ when "tui_state" then call_tui_state(args)
308
312
  when "tui_plain_text" then call_tui_plain_text
309
313
  when "tui_screenshot" then call_tui_screenshot(args)
310
314
  when "tui_html_render" then call_tui_html_render(args)
@@ -313,7 +317,7 @@ module TUITD
313
317
  when "tui_find_text" then call_tui_find_text(args)
314
318
  when "tui_close" then call_tui_close
315
319
  else
316
- return error_response(id, -32602, "Unknown tool: #{tool_name}")
320
+ return error_response(id, -32_602, "Unknown tool: #{tool_name}")
317
321
  end
318
322
 
319
323
  {
@@ -323,10 +327,10 @@ module TUITD
323
327
  content: [
324
328
  {
325
329
  type: "text",
326
- text: result
327
- }
328
- ]
329
- }
330
+ text: result,
331
+ },
332
+ ],
333
+ },
330
334
  }
331
335
  rescue TUITD::TimeoutError => e
332
336
  {
@@ -336,11 +340,11 @@ module TUITD
336
340
  content: [
337
341
  {
338
342
  type: "text",
339
- text: "TIMEOUT: #{e.message}"
340
- }
343
+ text: "TIMEOUT: #{e.message}",
344
+ },
341
345
  ],
342
- isError: false
343
- }
346
+ isError: false,
347
+ },
344
348
  }
345
349
  rescue StandardError => e
346
350
  {
@@ -350,11 +354,11 @@ module TUITD
350
354
  content: [
351
355
  {
352
356
  type: "text",
353
- text: "ERROR: #{e.class}: #{e.message}"
354
- }
357
+ text: "ERROR: #{e.class}: #{e.message}",
358
+ },
355
359
  ],
356
- isError: true
357
- }
360
+ isError: true,
361
+ },
358
362
  }
359
363
  end
360
364
 
@@ -400,7 +404,7 @@ module TUITD
400
404
  state_to_ai_text(state_data)
401
405
  end
402
406
 
403
- def call_tui_wait_for_stable(args)
407
+ def call_tui_wait_for_stable(_args)
404
408
  ensure_driver!
405
409
  @driver.wait_for_stable
406
410
 
@@ -431,7 +435,7 @@ module TUITD
431
435
 
432
436
  def call_tui_screenshot(args)
433
437
  ensure_driver!
434
- path = args["path"] || "/tmp/tui_td_#{Time.now.to_i}.png"
438
+ path = safe_path(args["path"], ext: "png")
435
439
  result = @driver.screenshot(path)
436
440
  "OK: Screenshot saved to #{result}"
437
441
  end
@@ -442,8 +446,9 @@ module TUITD
442
446
  renderer = HtmlRenderer.new(@driver.state_data)
443
447
 
444
448
  if path
445
- renderer.render(path)
446
- "OK: HTML saved to #{path}"
449
+ safe = safe_path(path, ext: "html")
450
+ renderer.render(safe)
451
+ "OK: HTML saved to #{safe}"
447
452
  else
448
453
  renderer.to_html
449
454
  end
@@ -491,6 +496,17 @@ module TUITD
491
496
 
492
497
  # --- Helpers ---
493
498
 
499
+ def safe_path(user_path, ext:)
500
+ default = File.join("/tmp", "tui_td_#{Time.now.to_i}.#{ext}")
501
+ resolved = File.expand_path(user_path || default)
502
+
503
+ unless ALLOWED_OUTPUT_DIRS.any? { |dir| resolved.start_with?(File.expand_path(dir)) }
504
+ raise TUITD::Error, "Output path must be under one of: #{ALLOWED_OUTPUT_DIRS.join(", ")}"
505
+ end
506
+
507
+ resolved
508
+ end
509
+
494
510
  def ensure_driver!
495
511
  raise Error, "No TUI session active. Call tui_start first." if @driver.nil?
496
512
  end
@@ -521,10 +537,11 @@ module TUITD
521
537
  id: id,
522
538
  error: {
523
539
  code: code,
524
- message: message
525
- }
540
+ message: message,
541
+ },
526
542
  }
527
543
  end
528
544
  end
529
545
  end
530
546
  end
547
+ # rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Layout/LineLength