tui-td 0.2.9 → 0.2.11
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 +17 -0
- data/README.md +16 -1
- data/lib/tui_td/ansi_parser.rb +139 -131
- data/lib/tui_td/ansi_utils.rb +22 -20
- data/lib/tui_td/cairo_renderer.rb +5 -2
- data/lib/tui_td/cli.rb +12 -10
- data/lib/tui_td/driver.rb +43 -12
- data/lib/tui_td/html_renderer.rb +19 -17
- data/lib/tui_td/matchers.rb +21 -12
- data/lib/tui_td/mcp/server.rb +146 -76
- data/lib/tui_td/screenshot.rb +70 -52
- data/lib/tui_td/state.rb +11 -4
- data/lib/tui_td/test_runner.rb +25 -25
- 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 +40 -11
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,10 +50,10 @@ 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
|
|
|
@@ -69,35 +71,35 @@ module TUITD
|
|
|
69
71
|
when "initialize"
|
|
70
72
|
handle_initialize(params, id)
|
|
71
73
|
when "notifications/initialized"
|
|
72
|
-
nil
|
|
74
|
+
nil # No response needed
|
|
73
75
|
when "tools/list"
|
|
74
76
|
handle_tools_list(id)
|
|
75
77
|
when "tools/call"
|
|
76
78
|
handle_tools_call(params, id)
|
|
77
79
|
else
|
|
78
80
|
if method&.start_with?("notifications/")
|
|
79
|
-
nil
|
|
81
|
+
nil # Ignore unknown notifications
|
|
80
82
|
else
|
|
81
|
-
error_response(id, -
|
|
83
|
+
error_response(id, -32_601, "Method not found: #{method}")
|
|
82
84
|
end
|
|
83
85
|
end
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
# Initialize handshake
|
|
87
|
-
def handle_initialize(
|
|
89
|
+
def handle_initialize(_params, id)
|
|
88
90
|
{
|
|
89
91
|
jsonrpc: "2.0",
|
|
90
92
|
id: id,
|
|
91
93
|
result: {
|
|
92
94
|
protocolVersion: PROTOCOL_VERSION,
|
|
93
95
|
capabilities: {
|
|
94
|
-
tools: {}
|
|
96
|
+
tools: {},
|
|
95
97
|
},
|
|
96
98
|
serverInfo: {
|
|
97
99
|
name: SERVER_NAME,
|
|
98
|
-
version: SERVER_VERSION
|
|
99
|
-
}
|
|
100
|
-
}
|
|
100
|
+
version: SERVER_VERSION,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
101
103
|
}
|
|
102
104
|
end
|
|
103
105
|
|
|
@@ -116,26 +118,26 @@ module TUITD
|
|
|
116
118
|
properties: {
|
|
117
119
|
command: {
|
|
118
120
|
type: "string",
|
|
119
|
-
description: "The command to run (e.g., 'htop', 'vim file.txt')"
|
|
121
|
+
description: "The command to run (e.g., 'htop', 'vim file.txt')",
|
|
120
122
|
},
|
|
121
123
|
rows: {
|
|
122
124
|
type: "integer",
|
|
123
125
|
description: "Terminal height in rows (default: 40)",
|
|
124
|
-
default: 40
|
|
126
|
+
default: 40,
|
|
125
127
|
},
|
|
126
128
|
cols: {
|
|
127
129
|
type: "integer",
|
|
128
130
|
description: "Terminal width in columns (default: 120)",
|
|
129
|
-
default: 120
|
|
131
|
+
default: 120,
|
|
130
132
|
},
|
|
131
133
|
timeout: {
|
|
132
134
|
type: "integer",
|
|
133
135
|
description: "Timeout in seconds for waits (default: 30)",
|
|
134
|
-
default: 30
|
|
135
|
-
}
|
|
136
|
+
default: 30,
|
|
137
|
+
},
|
|
136
138
|
},
|
|
137
|
-
required: ["command"]
|
|
138
|
-
}
|
|
139
|
+
required: ["command"],
|
|
140
|
+
},
|
|
139
141
|
},
|
|
140
142
|
{
|
|
141
143
|
name: "tui_send",
|
|
@@ -145,11 +147,11 @@ module TUITD
|
|
|
145
147
|
properties: {
|
|
146
148
|
text: {
|
|
147
149
|
type: "string",
|
|
148
|
-
description: "Text to send to the TUI. Use \\n for enter/newline."
|
|
149
|
-
}
|
|
150
|
+
description: "Text to send to the TUI. Use \\n for enter/newline.",
|
|
151
|
+
},
|
|
150
152
|
},
|
|
151
|
-
required: ["text"]
|
|
152
|
-
}
|
|
153
|
+
required: ["text"],
|
|
154
|
+
},
|
|
153
155
|
},
|
|
154
156
|
{
|
|
155
157
|
name: "tui_send_key",
|
|
@@ -159,14 +161,14 @@ module TUITD
|
|
|
159
161
|
properties: {
|
|
160
162
|
key: {
|
|
161
163
|
type: "string",
|
|
162
|
-
enum: [
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
description: "Key to press"
|
|
166
|
-
}
|
|
164
|
+
enum: %w[enter tab escape up down left right
|
|
165
|
+
backspace ctrl_c ctrl_d ctrl_z page_up page_down
|
|
166
|
+
home end delete],
|
|
167
|
+
description: "Key to press",
|
|
168
|
+
},
|
|
167
169
|
},
|
|
168
|
-
required: ["key"]
|
|
169
|
-
}
|
|
170
|
+
required: ["key"],
|
|
171
|
+
},
|
|
170
172
|
},
|
|
171
173
|
{
|
|
172
174
|
name: "tui_wait_for_text",
|
|
@@ -176,16 +178,16 @@ module TUITD
|
|
|
176
178
|
properties: {
|
|
177
179
|
text: {
|
|
178
180
|
type: "string",
|
|
179
|
-
description: "Text to wait for in the terminal output"
|
|
181
|
+
description: "Text to wait for in the terminal output",
|
|
180
182
|
},
|
|
181
183
|
timeout: {
|
|
182
184
|
type: "integer",
|
|
183
185
|
description: "Custom timeout in seconds (overrides default)",
|
|
184
|
-
default: 30
|
|
185
|
-
}
|
|
186
|
+
default: 30,
|
|
187
|
+
},
|
|
186
188
|
},
|
|
187
|
-
required: ["text"]
|
|
188
|
-
}
|
|
189
|
+
required: ["text"],
|
|
190
|
+
},
|
|
189
191
|
},
|
|
190
192
|
{
|
|
191
193
|
name: "tui_wait_for_stable",
|
|
@@ -196,10 +198,10 @@ module TUITD
|
|
|
196
198
|
timeout: {
|
|
197
199
|
type: "integer",
|
|
198
200
|
description: "Custom timeout in seconds",
|
|
199
|
-
default: 30
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
201
|
+
default: 30,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
203
205
|
},
|
|
204
206
|
{
|
|
205
207
|
name: "tui_state",
|
|
@@ -209,20 +211,20 @@ module TUITD
|
|
|
209
211
|
properties: {
|
|
210
212
|
format: {
|
|
211
213
|
type: "string",
|
|
212
|
-
enum: [
|
|
214
|
+
enum: %w[ai full text],
|
|
213
215
|
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
|
-
}
|
|
216
|
+
default: "ai",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
218
220
|
},
|
|
219
221
|
{
|
|
220
222
|
name: "tui_plain_text",
|
|
221
223
|
description: "Get the current terminal content as plain text (all ANSI stripped).",
|
|
222
224
|
inputSchema: {
|
|
223
225
|
type: "object",
|
|
224
|
-
properties: {}
|
|
225
|
-
}
|
|
226
|
+
properties: {},
|
|
227
|
+
},
|
|
226
228
|
},
|
|
227
229
|
{
|
|
228
230
|
name: "tui_screenshot",
|
|
@@ -232,10 +234,10 @@ module TUITD
|
|
|
232
234
|
properties: {
|
|
233
235
|
path: {
|
|
234
236
|
type: "string",
|
|
235
|
-
description: "Output file path (optional, auto-generated if omitted)"
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
237
|
+
description: "Output file path (optional, auto-generated if omitted)",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
239
241
|
},
|
|
240
242
|
{
|
|
241
243
|
name: "tui_html_render",
|
|
@@ -245,21 +247,51 @@ module TUITD
|
|
|
245
247
|
properties: {
|
|
246
248
|
path: {
|
|
247
249
|
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
|
-
}
|
|
250
|
+
description: "Optional file path to save the HTML. If omitted, the HTML content is returned inline so you can view it directly.",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "tui_wait_for_exit",
|
|
257
|
+
description: "Wait until the TUI process exits. Returns the exit status code (0 = success, non-zero = error).",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "tui_exit_status",
|
|
265
|
+
description: "Get the exit status of the TUI process. Returns nil if still running, otherwise the exit code.",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "tui_find_text",
|
|
273
|
+
description: "Search for text or regex pattern in the current terminal state. Returns positions of all matches with surrounding context.",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
pattern: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "Text or regex pattern to search for (e.g., 'error', 'ERROR|FAIL')",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
required: ["pattern"],
|
|
283
|
+
},
|
|
252
284
|
},
|
|
253
285
|
{
|
|
254
286
|
name: "tui_close",
|
|
255
287
|
description: "Close the TUI application and clean up the PTY session. Call this when finished.",
|
|
256
288
|
inputSchema: {
|
|
257
289
|
type: "object",
|
|
258
|
-
properties: {}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
}
|
|
290
|
+
properties: {},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
263
295
|
}
|
|
264
296
|
end
|
|
265
297
|
|
|
@@ -274,13 +306,16 @@ module TUITD
|
|
|
274
306
|
when "tui_send_key" then call_tui_send_key(args)
|
|
275
307
|
when "tui_wait_for_text" then call_tui_wait_for_text(args)
|
|
276
308
|
when "tui_wait_for_stable" then call_tui_wait_for_stable(args)
|
|
277
|
-
when "tui_state"
|
|
309
|
+
when "tui_state" then call_tui_state(args)
|
|
278
310
|
when "tui_plain_text" then call_tui_plain_text
|
|
279
311
|
when "tui_screenshot" then call_tui_screenshot(args)
|
|
280
312
|
when "tui_html_render" then call_tui_html_render(args)
|
|
313
|
+
when "tui_wait_for_exit" then call_tui_wait_for_exit
|
|
314
|
+
when "tui_exit_status" then call_tui_exit_status
|
|
315
|
+
when "tui_find_text" then call_tui_find_text(args)
|
|
281
316
|
when "tui_close" then call_tui_close
|
|
282
317
|
else
|
|
283
|
-
return error_response(id, -
|
|
318
|
+
return error_response(id, -32_602, "Unknown tool: #{tool_name}")
|
|
284
319
|
end
|
|
285
320
|
|
|
286
321
|
{
|
|
@@ -290,10 +325,10 @@ module TUITD
|
|
|
290
325
|
content: [
|
|
291
326
|
{
|
|
292
327
|
type: "text",
|
|
293
|
-
text: result
|
|
294
|
-
}
|
|
295
|
-
]
|
|
296
|
-
}
|
|
328
|
+
text: result,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
297
332
|
}
|
|
298
333
|
rescue TUITD::TimeoutError => e
|
|
299
334
|
{
|
|
@@ -303,11 +338,11 @@ module TUITD
|
|
|
303
338
|
content: [
|
|
304
339
|
{
|
|
305
340
|
type: "text",
|
|
306
|
-
text: "TIMEOUT: #{e.message}"
|
|
307
|
-
}
|
|
341
|
+
text: "TIMEOUT: #{e.message}",
|
|
342
|
+
},
|
|
308
343
|
],
|
|
309
|
-
isError: false
|
|
310
|
-
}
|
|
344
|
+
isError: false,
|
|
345
|
+
},
|
|
311
346
|
}
|
|
312
347
|
rescue StandardError => e
|
|
313
348
|
{
|
|
@@ -317,11 +352,11 @@ module TUITD
|
|
|
317
352
|
content: [
|
|
318
353
|
{
|
|
319
354
|
type: "text",
|
|
320
|
-
text: "ERROR: #{e.class}: #{e.message}"
|
|
321
|
-
}
|
|
355
|
+
text: "ERROR: #{e.class}: #{e.message}",
|
|
356
|
+
},
|
|
322
357
|
],
|
|
323
|
-
isError: true
|
|
324
|
-
}
|
|
358
|
+
isError: true,
|
|
359
|
+
},
|
|
325
360
|
}
|
|
326
361
|
end
|
|
327
362
|
|
|
@@ -367,7 +402,7 @@ module TUITD
|
|
|
367
402
|
state_to_ai_text(state_data)
|
|
368
403
|
end
|
|
369
404
|
|
|
370
|
-
def call_tui_wait_for_stable(
|
|
405
|
+
def call_tui_wait_for_stable(_args)
|
|
371
406
|
ensure_driver!
|
|
372
407
|
@driver.wait_for_stable
|
|
373
408
|
|
|
@@ -416,6 +451,40 @@ module TUITD
|
|
|
416
451
|
end
|
|
417
452
|
end
|
|
418
453
|
|
|
454
|
+
def call_tui_wait_for_exit
|
|
455
|
+
ensure_driver!
|
|
456
|
+
@driver.wait_for_exit
|
|
457
|
+
status = @driver.exitstatus
|
|
458
|
+
"OK: Process exited with status #{status}"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def call_tui_exit_status
|
|
462
|
+
ensure_driver!
|
|
463
|
+
status = @driver.exitstatus
|
|
464
|
+
if status.nil?
|
|
465
|
+
"Process is still running"
|
|
466
|
+
else
|
|
467
|
+
"Exit status: #{status}"
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def call_tui_find_text(args)
|
|
472
|
+
ensure_driver!
|
|
473
|
+
pattern = args["pattern"] or return "ERROR: 'pattern' argument is required"
|
|
474
|
+
state = TUITD::State.new(@driver.state_data)
|
|
475
|
+
matches = state.find_text(pattern)
|
|
476
|
+
|
|
477
|
+
if matches.empty?
|
|
478
|
+
"No matches found for: #{pattern}"
|
|
479
|
+
else
|
|
480
|
+
lines = ["Found #{matches.size} match(es) for: #{pattern}"]
|
|
481
|
+
matches.each do |m|
|
|
482
|
+
lines << " row #{m[:row]}, col #{m[:col]}: #{m[:full_line].strip}"
|
|
483
|
+
end
|
|
484
|
+
lines.join("\n")
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
419
488
|
def call_tui_close
|
|
420
489
|
@driver&.close
|
|
421
490
|
@driver = nil
|
|
@@ -454,10 +523,11 @@ module TUITD
|
|
|
454
523
|
id: id,
|
|
455
524
|
error: {
|
|
456
525
|
code: code,
|
|
457
|
-
message: message
|
|
458
|
-
}
|
|
526
|
+
message: message,
|
|
527
|
+
},
|
|
459
528
|
}
|
|
460
529
|
end
|
|
461
530
|
end
|
|
462
531
|
end
|
|
463
532
|
end
|
|
533
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Layout/LineLength
|