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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/lib/tui_td/ansi_parser.rb +3 -719
- data/lib/tui_td/ansi_utils.rb +3 -71
- data/lib/tui_td/cairo_renderer.rb +5 -2
- data/lib/tui_td/cli.rb +12 -10
- data/lib/tui_td/driver.rb +54 -14
- data/lib/tui_td/html_renderer.rb +19 -17
- data/lib/tui_td/matchers.rb +21 -12
- data/lib/tui_td/mcp/server.rb +104 -87
- data/lib/tui_td/screenshot.rb +70 -52
- data/lib/tui_td/state.rb +3 -117
- data/lib/tui_td/test_runner.rb +41 -27
- data/lib/tui_td/unifont_glyphs.rb +2142 -2141
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +7 -3
- metadata +50 -7
data/lib/tui_td/mcp/server.rb
CHANGED
|
@@ -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
|
-
|
|
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, -
|
|
53
|
+
error_response(nil, -32_700, "Parse error: #{e.message}")
|
|
52
54
|
rescue StandardError => e
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
|
83
|
+
nil # Ignore unknown notifications
|
|
80
84
|
else
|
|
81
|
-
error_response(id, -
|
|
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(
|
|
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: [
|
|
163
|
-
|
|
164
|
-
|
|
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: [
|
|
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"
|
|
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, -
|
|
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(
|
|
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"]
|
|
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
|
-
|
|
446
|
-
|
|
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
|