ntermqt 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl

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.
@@ -7,11 +7,13 @@ Provides a safe, policy-controlled interface to network devices.
7
7
  Same interface used by both humans (IPython) and MCP tools.
8
8
  """
9
9
 
10
- from nterm.scripting.repl import NTermREPL, REPLPolicy
11
- from .api import NTermAPI
12
- from typing import Optional
13
10
  import sys
14
11
  import getpass
12
+ import json
13
+ from typing import Optional, Dict
14
+
15
+ from .repl import NTermREPL, REPLPolicy
16
+ from .api import NTermAPI
15
17
 
16
18
 
17
19
  def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = None):
@@ -39,8 +41,8 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
39
41
  api.repl(policy=policy)
40
42
  """
41
43
  if api is None:
42
- from nterm.scripting import api as default_api
43
- api = default_api
44
+ from .api import NTermAPI
45
+ api = NTermAPI()
44
46
 
45
47
  repl = NTermREPL(api=api, policy=policy)
46
48
 
@@ -67,7 +69,6 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
67
69
  print(f"\n⚠️ TextFSM database seems small ({db_info.get('db_size_mb', 0):.1f} MB)")
68
70
  print(f" Expected ~0.3 MB. Use :dbinfo to check.")
69
71
  except Exception:
70
- # Don't crash startup if db_info fails
71
72
  pass
72
73
 
73
74
  print()
@@ -77,21 +78,13 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
77
78
  # Interactive loop
78
79
  try:
79
80
  while True:
80
- # Show prompt with mode indicator
81
- if repl.state.connected_device:
82
- mode_indicator = "📊" if repl.state.output_mode == "parsed" else "📄"
83
- hint = f"[{repl.state.platform_hint}]" if repl.state.platform_hint else ""
84
- prompt = f"{mode_indicator} {repl.state.connected_device}{hint}> "
85
- else:
86
- prompt = "nterm> "
81
+ prompt = _build_prompt(repl)
87
82
 
88
83
  try:
89
84
  line = input(prompt)
90
85
  except EOFError:
91
- # Ctrl+D
92
86
  break
93
87
  except KeyboardInterrupt:
94
- # Ctrl+C
95
88
  print()
96
89
  continue
97
90
 
@@ -99,185 +92,329 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
99
92
  result = repl.handle_line(line)
100
93
 
101
94
  # Display result
102
- if not result.get("ok"):
103
- print(f"Error: {result.get('error')}")
104
- continue
105
-
106
- data = result.get("data", {})
107
- cmd_type = data.get("type")
95
+ _display_result(repl, result)
108
96
 
109
- if cmd_type == "exit":
97
+ # Check for exit
98
+ if result.get("ok") and result.get("data", {}).get("type") == "exit":
110
99
  break
111
- elif cmd_type == "unlock_prompt":
112
- # Securely prompt for password
113
- try:
114
- password = getpass.getpass("Enter vault password: ")
115
- unlock_result = repl.do_unlock(password)
116
- if unlock_result.get("ok"):
117
- unlock_data = unlock_result.get("data", {})
118
- if unlock_data.get("vault_unlocked"):
119
- print("Vault unlocked")
120
- else:
121
- print("Unlock failed - incorrect password")
122
- else:
123
- print(f"Error: {unlock_result.get('error')}")
124
- except KeyboardInterrupt:
125
- print("\nUnlock cancelled")
126
- continue
127
- elif cmd_type == "help":
128
- print(data.get("text", ""))
129
- elif cmd_type == "unlock":
130
- status = "unlocked" if data.get("vault_unlocked") else "failed"
131
- print(f"Vault: {status}")
132
- elif cmd_type == "lock":
133
- print("Vault locked")
134
- elif cmd_type == "credentials":
135
- creds = data.get("credentials", [])
136
- if not creds:
137
- print("No credentials found")
138
- else:
139
- print(f"\n{'Name':<20} {'Username':<20} {'Type':<15}")
140
- print("-" * 55)
141
- for cred in creds:
142
- print(f"{cred['name']:<20} {cred.get('username', ''):<20} {cred.get('cred_type', 'ssh'):<15}")
143
- print()
144
- elif cmd_type == "devices":
145
- devices = data.get("devices", [])
146
- if not devices:
147
- print("No devices found")
148
- else:
149
- print(f"\n{'Name':<20} {'Hostname':<20} {'Folder':<15}")
150
- print("-" * 55)
151
- for dev in devices:
152
- print(f"{dev['name']:<20} {dev['hostname']:<20} {dev.get('folder', ''):<15}")
153
- print()
154
- elif cmd_type == "connect":
155
- print(f"Connected to {data['device']} ({data['hostname']}:{data['port']})")
156
- print(f"Platform: {data.get('platform', 'unknown')}")
157
- print(f"Prompt: {data.get('prompt', '')}")
158
- elif cmd_type == "disconnect":
159
- print("Disconnected")
160
- elif cmd_type == "policy":
161
- print(f"Policy mode: {data.get('mode')}")
162
- elif cmd_type == "mode":
163
- mode = data.get('mode')
164
- hint = data.get('platform_hint')
165
- if mode:
166
- print(f"Output mode: {mode}")
167
- else:
168
- print(f"Current mode: {mode}")
169
- if hint:
170
- print(f"Platform hint: {hint}")
171
- elif cmd_type == "format":
172
- fmt = data.get('format')
173
- print(f"Output format: {fmt}")
174
- elif cmd_type == "set_hint":
175
- print(f"Platform hint set to: {data.get('platform_hint')}")
176
- elif cmd_type == "clear_hint":
177
- print("Platform hint cleared (using auto-detection)")
178
- elif cmd_type == "debug":
179
- status = "ON" if data.get("debug_mode") else "OFF"
180
- print(f"Debug mode: {status}")
181
- elif cmd_type == "dbinfo":
182
- db_info = data.get("db_info", {})
183
- print("\nTextFSM Database Info:")
184
- print("=" * 60)
185
- print(f"Engine Available: {db_info.get('engine_available', False)}")
186
- print(f"Database Path: {db_info.get('db_path', 'unknown')}")
187
- print(f"Database Exists: {db_info.get('db_exists', False)}")
188
-
189
- if db_info.get('db_exists'):
190
- db_size = db_info.get('db_size', 0)
191
- db_size_mb = db_info.get('db_size_mb', 0.0)
192
- print(f"Database Size: {db_size:,} bytes ({db_size_mb:.1f} MB)")
193
- print(f"Absolute Path: {db_info.get('db_absolute_path', 'unknown')}")
194
-
195
- # Health checks
196
- if db_size == 0:
197
- print("\n⚠️ WARNING: Database file is empty (0 bytes)!")
198
- print(" Parsing will fail until you download templates.")
199
- print(" Run: api.download_templates() or use the templates installer.")
200
- elif db_size < 100000: # Less than 100KB
201
- print(f"\n⚠️ WARNING: Database seems too small ({db_size_mb:.1f} MB)")
202
- print(" Expected size is ~0.3 MB. May be corrupted or incomplete.")
203
- else:
204
- print("\n✓ Database appears healthy")
205
- else:
206
- print("\n❌ ERROR: Database file not found!")
207
- print(" Run: api.download_templates() to create it.")
208
100
 
209
- print()
210
- elif cmd_type == "result":
211
- result_data = data.get("result", {})
212
-
213
- # Debug mode: show full result dict
214
- if repl.state.debug_mode:
215
- print("\n[DEBUG - Full Result Dict]")
216
- print("-" * 60)
217
- import json
218
- # Don't print raw_output in debug to avoid clutter
219
- debug_data = {k: v for k, v in result_data.items() if k != "raw_output"}
220
- print(json.dumps(debug_data, indent=2))
221
- print("-" * 60)
222
-
223
- # Show parsed data if available
224
- parsed = result_data.get("parsed_data")
225
- parse_success = result_data.get("parse_success", False)
226
- platform = result_data.get("platform", "")
227
-
228
- # Display based on mode and format
229
- if repl.state.output_mode == "parsed":
230
- if parsed and parse_success:
231
- print(f"\n[Parsed with {platform} - format: {repl.state.output_format}]")
232
- print("-" * 60)
233
- _display_parsed_result(parsed, repl.state.output_format)
234
- print()
235
- elif parse_success and not parsed:
236
- # Parsing succeeded but returned empty/no data
237
- print(f"\n[Parsed with {platform} - no structured data]")
238
- raw = result_data.get("raw_output", "")
239
- print(raw)
240
- elif parsed is None and not parse_success:
241
- # Parsing failed or wasn't attempted
242
- print(f"\n[Parse failed - showing raw output]")
243
- raw = result_data.get("raw_output", "")
244
- print(raw)
245
- else:
246
- # Fallback
247
- raw = result_data.get("raw_output", "")
248
- print(raw)
101
+ finally:
102
+ # Clean up
103
+ count = repl.state.api.disconnect_all()
104
+ if count > 0:
105
+ print(f"\nDisconnected {count} session(s)")
106
+ print("REPL closed")
107
+
108
+
109
+ def _build_prompt(repl: NTermREPL) -> str:
110
+ """Build the prompt string based on current state."""
111
+ if repl.state.connected_device:
112
+ mode_indicator = "📊" if repl.state.output_mode == "parsed" else "📄"
113
+ hint = f"[{repl.state.platform_hint}]" if repl.state.platform_hint else ""
114
+ return f"{mode_indicator} {repl.state.connected_device}{hint}> "
115
+ else:
116
+ return "nterm> "
117
+
118
+
119
+ def _display_result(repl: NTermREPL, result: Dict) -> None:
120
+ """Display REPL result based on type."""
121
+ if not result.get("ok"):
122
+ print(f"Error: {result.get('error')}")
123
+ return
124
+
125
+ data = result.get("data", {})
126
+ cmd_type = data.get("type")
127
+
128
+ # ===== Vault Commands =====
129
+ if cmd_type == "unlock_prompt":
130
+ try:
131
+ password = getpass.getpass("Enter vault password: ")
132
+ unlock_result = repl.do_unlock(password)
133
+ if unlock_result.get("ok"):
134
+ if unlock_result.get("data", {}).get("vault_unlocked"):
135
+ print(" Vault unlocked")
249
136
  else:
250
- # Raw mode - just show output
251
- raw = result_data.get("raw_output", "")
252
- print(raw)
253
-
254
- # Show timing
255
- elapsed = result_data.get("elapsed_seconds", 0)
256
- print(f"\n[{elapsed}s]")
257
- elif cmd_type == "noop":
258
- pass
137
+ print("✗ Unlock failed - incorrect password")
259
138
  else:
260
- # Unknown type, show raw data
261
- import json
262
- print(json.dumps(data, indent=2))
139
+ print(f"Error: {unlock_result.get('error')}")
140
+ except KeyboardInterrupt:
141
+ print("\nUnlock cancelled")
142
+ return
143
+
144
+ if cmd_type == "unlock":
145
+ status = "unlocked" if data.get("vault_unlocked") else "failed"
146
+ print(f"Vault: {status}")
147
+ return
148
+
149
+ if cmd_type == "lock":
150
+ print("✓ Vault locked")
151
+ return
152
+
153
+ # ===== Inventory Commands =====
154
+ if cmd_type == "credentials":
155
+ creds = data.get("credentials", [])
156
+ if not creds:
157
+ print("No credentials found")
158
+ else:
159
+ print(f"\n{'Name':<20} {'Username':<20} {'Auth':<15} {'Default':<8}")
160
+ print("-" * 63)
161
+ for cred in creds:
162
+ auth_types = []
163
+ if cred.get('has_password'):
164
+ auth_types.append('pass')
165
+ if cred.get('has_key'):
166
+ auth_types.append('key')
167
+ auth = '+'.join(auth_types) if auth_types else 'none'
168
+ default = '★' if cred.get('is_default') else ''
169
+ print(f"{cred['name']:<20} {cred.get('username', ''):<20} {auth:<15} {default:<8}")
170
+ print()
171
+ return
172
+
173
+ if cmd_type == "devices":
174
+ devices = data.get("devices", [])
175
+ if not devices:
176
+ print("No devices found")
177
+ else:
178
+ print(f"\n{'Name':<25} {'Hostname':<20} {'Port':<6} {'Folder':<15}")
179
+ print("-" * 66)
180
+ for dev in devices:
181
+ print(f"{dev['name']:<25} {dev['hostname']:<20} {dev.get('port', 22):<6} {dev.get('folder', ''):<15}")
182
+ print(f"\n{len(devices)} device(s)")
183
+ return
184
+
185
+ if cmd_type == "folders":
186
+ folders = data.get("folders", [])
187
+ if not folders:
188
+ print("No folders found")
189
+ else:
190
+ print("\nFolders:")
191
+ for f in sorted(folders):
192
+ print(f" 📁 {f}")
193
+ print()
194
+ return
195
+
196
+ # ===== Session Commands =====
197
+ if cmd_type == "connect":
198
+ print(f"✓ Connected to {data['device']} ({data['hostname']}:{data['port']})")
199
+ print(f" Platform: {data.get('platform', 'unknown')}")
200
+ print(f" Prompt: {data.get('prompt', '')}")
201
+ return
202
+
203
+ if cmd_type == "disconnect":
204
+ msg = data.get("message", "Disconnected")
205
+ print(f"✓ {msg}")
206
+ return
207
+
208
+ if cmd_type == "sessions":
209
+ sessions = data.get("sessions", [])
210
+ current = data.get("current")
211
+ if not sessions:
212
+ print("No active sessions")
213
+ else:
214
+ print(f"\n{'Device':<20} {'Hostname':<20} {'Platform':<15} {'Status':<10}")
215
+ print("-" * 65)
216
+ for s in sessions:
217
+ marker = "→ " if s['device'] == current else " "
218
+ status = "connected" if s.get('connected') else "stale"
219
+ print(f"{marker}{s['device']:<18} {s['hostname']:<20} {s.get('platform', 'unknown'):<15} {status:<10}")
220
+ print()
221
+ return
222
+
223
+ # ===== Settings Commands =====
224
+ if cmd_type == "policy":
225
+ mode = data.get('mode')
226
+ emoji = "🔒" if mode == "read_only" else "⚡"
227
+ print(f"Policy mode: {emoji} {mode}")
228
+ return
229
+
230
+ if cmd_type == "mode":
231
+ mode = data.get('mode')
232
+ hint = data.get('platform_hint')
233
+ print(f"Output mode: {mode}")
234
+ if hint:
235
+ print(f"Platform hint: {hint}")
236
+ return
237
+
238
+ if cmd_type == "format":
239
+ print(f"Output format: {data.get('format')}")
240
+ return
241
+
242
+ if cmd_type == "set_hint":
243
+ print(f"✓ Platform hint set to: {data.get('platform_hint')}")
244
+ return
245
+
246
+ if cmd_type == "clear_hint":
247
+ print("✓ Platform hint cleared (using auto-detection)")
248
+ return
249
+
250
+ if cmd_type == "debug":
251
+ status = "ON" if data.get("debug_mode") else "OFF"
252
+ print(f"Debug mode: {status}")
253
+ return
254
+
255
+ # ===== Info Commands =====
256
+ if cmd_type == "dbinfo":
257
+ _display_dbinfo(data.get("db_info", {}))
258
+ return
259
+
260
+ if cmd_type == "help":
261
+ print(data.get("text", ""))
262
+ return
263
+
264
+ if cmd_type == "exit":
265
+ count = data.get("disconnected", 0)
266
+ if count > 0:
267
+ print(f"Disconnected {count} session(s)")
268
+ return
269
+
270
+ if cmd_type == "noop":
271
+ return
272
+
273
+ # ===== Quick Commands =====
274
+ if cmd_type == "version":
275
+ result_data = data.get("result", {})
276
+ version_info = result_data.get("version_info", {})
277
+
278
+ print(f"\n{'─' * 50}")
279
+ print(f" Version: {version_info.get('version', 'unknown')}")
280
+ print(f" Hardware: {version_info.get('hardware', 'unknown')}")
281
+ print(f" Serial: {version_info.get('serial', 'unknown')}")
282
+ print(f" Uptime: {version_info.get('uptime', 'unknown')}")
283
+ print(f"{'─' * 50}")
284
+
285
+ elapsed = result_data.get("elapsed_seconds", 0)
286
+ print(f"[{elapsed}s]")
287
+ return
288
+
289
+ if cmd_type == "neighbors":
290
+ result_data = data.get("result", {})
291
+ neighbor_info = result_data.get("neighbor_info", [])
292
+
293
+ if not neighbor_info:
294
+ parsed = result_data.get("parsed_data", [])
295
+ if parsed:
296
+ # Fall back to raw parsed data
297
+ _display_parsed_result(parsed, repl.state.output_format)
298
+ else:
299
+ print("No neighbors found")
300
+ else:
301
+ print(f"\n{'Local Interface':<20} {'Neighbor':<30} {'Remote Port':<20} {'Platform':<20}")
302
+ print("-" * 90)
303
+ for n in neighbor_info:
304
+ print(f"{n.get('local_interface', 'unknown'):<20} "
305
+ f"{n.get('neighbor_device', 'unknown'):<30} "
306
+ f"{n.get('neighbor_interface', 'unknown'):<20} "
307
+ f"{n.get('platform', '')[:20]:<20}")
308
+ print(f"\n{len(neighbor_info)} neighbor(s)")
309
+
310
+ elapsed = result_data.get("elapsed_seconds", 0)
311
+ print(f"[{elapsed}s]")
312
+ return
313
+
314
+ # ===== Generic Result =====
315
+ if cmd_type == "result":
316
+ result_data = data.get("result", {})
317
+ _display_command_result(repl, result_data)
318
+ return
319
+
320
+ # Unknown type - dump as JSON
321
+ print(json.dumps(data, indent=2))
322
+
323
+
324
+ def _display_command_result(repl: NTermREPL, result_data: Dict) -> None:
325
+ """Display a generic command result."""
326
+ # Debug mode: show full result dict
327
+ if repl.state.debug_mode:
328
+ print("\n[DEBUG - Full Result Dict]")
329
+ print("-" * 60)
330
+ debug_data = {k: v for k, v in result_data.items() if k != "raw_output"}
331
+ print(json.dumps(debug_data, indent=2, default=str))
332
+ print("-" * 60)
333
+
334
+ parsed = result_data.get("parsed_data")
335
+ parse_success = result_data.get("parse_success", False)
336
+ platform = result_data.get("platform", "")
337
+ command_type = result_data.get("command_type", "")
338
+
339
+ # Display based on mode
340
+ if repl.state.output_mode == "parsed":
341
+ if parsed and parse_success:
342
+ header = f"[Parsed: {platform}"
343
+ if command_type:
344
+ header += f" | {command_type}"
345
+ header += f" | format: {repl.state.output_format}]"
346
+ print(f"\n{header}")
347
+ print("-" * 60)
348
+ _display_parsed_result(parsed, repl.state.output_format)
349
+ print()
350
+ elif parse_success and not parsed:
351
+ print(f"\n[Parsed: {platform} - no structured data]")
352
+ raw = result_data.get("raw_output", "")
353
+ print(raw)
354
+ elif not parse_success:
355
+ print(f"\n[Parse failed - showing raw output]")
356
+ raw = result_data.get("raw_output", "")
357
+ print(raw)
358
+ else:
359
+ raw = result_data.get("raw_output", "")
360
+ print(raw)
361
+ else:
362
+ # Raw mode
363
+ raw = result_data.get("raw_output", "")
364
+ print(raw)
365
+
366
+ # Show timing
367
+ elapsed = result_data.get("elapsed_seconds", 0)
368
+ print(f"\n[{elapsed}s]")
369
+
370
+
371
+ def _display_dbinfo(db_info: Dict) -> None:
372
+ """Display TextFSM database info."""
373
+ print("\nTextFSM Database Info:")
374
+ print("=" * 60)
375
+ print(f"Engine Available: {db_info.get('engine_available', False)}")
376
+ print(f"Database Path: {db_info.get('db_path', 'unknown')}")
377
+ print(f"Database Exists: {db_info.get('db_exists', False)}")
378
+
379
+ if db_info.get('db_exists'):
380
+ db_size = db_info.get('db_size', 0)
381
+ db_size_mb = db_info.get('db_size_mb', 0.0)
382
+ print(f"Database Size: {db_size:,} bytes ({db_size_mb:.1f} MB)")
383
+ print(f"Absolute Path: {db_info.get('db_absolute_path', 'unknown')}")
384
+
385
+ if db_size == 0:
386
+ print("\n⚠️ WARNING: Database file is empty (0 bytes)!")
387
+ print(" Parsing will fail until you download templates.")
388
+ elif db_size < 100000:
389
+ print(f"\n⚠️ WARNING: Database seems too small ({db_size_mb:.1f} MB)")
390
+ print(" Expected size is ~0.3 MB. May be corrupted or incomplete.")
391
+ else:
392
+ print("\n✓ Database appears healthy")
393
+ else:
394
+ print("\n✗ ERROR: Database file not found!")
395
+ print(" Run: api.download_templates() to create it.")
396
+
397
+ print()
263
398
 
264
- finally:
265
- # Clean up
266
- if repl.state.session:
267
- print("\nDisconnecting...")
268
- repl._safe_disconnect()
269
- print("\nREPL closed")
399
+
400
+ def _display_parsed_result(data, output_format: str, max_rows: int = 20) -> None:
401
+ """Display parsed data in the specified format."""
402
+ if output_format == "json":
403
+ print(json.dumps(data, indent=2, default=str))
404
+ elif output_format == "rich":
405
+ _print_parsed_data_rich(data, max_rows)
406
+ else:
407
+ _print_parsed_data_text(data, max_rows)
270
408
 
271
409
 
272
- def _print_parsed_data(data, max_rows=20):
273
- """Pretty print parsed data (list of dicts) in text format."""
410
+ def _print_parsed_data_text(data, max_rows: int = 20) -> None:
411
+ """Pretty print parsed data as text table."""
274
412
  if not data:
275
413
  print("(empty)")
276
414
  return
277
415
 
278
416
  if not isinstance(data, list):
279
- import json
280
- print(json.dumps(data, indent=2))
417
+ print(json.dumps(data, indent=2, default=str))
281
418
  return
282
419
 
283
420
  # Get all unique keys
@@ -289,14 +426,11 @@ def _print_parsed_data(data, max_rows=20):
289
426
  keys = sorted(all_keys)
290
427
 
291
428
  if not keys:
292
- import json
293
- print(json.dumps(data, indent=2))
429
+ print(json.dumps(data, indent=2, default=str))
294
430
  return
295
431
 
296
432
  # Calculate column widths
297
- col_widths = {}
298
- for key in keys:
299
- col_widths[key] = len(key)
433
+ col_widths = {key: len(key) for key in keys}
300
434
 
301
435
  for row in data[:max_rows]:
302
436
  if isinstance(row, dict):
@@ -304,7 +438,7 @@ def _print_parsed_data(data, max_rows=20):
304
438
  val = str(row.get(key, ""))
305
439
  col_widths[key] = max(col_widths[key], len(val))
306
440
 
307
- # Cap widths
441
+ # Cap widths at 30
308
442
  for key in keys:
309
443
  col_widths[key] = min(col_widths[key], 30)
310
444
 
@@ -334,14 +468,14 @@ def _print_parsed_data(data, max_rows=20):
334
468
  shown += 1
335
469
 
336
470
 
337
- def _print_parsed_data_rich(data, max_rows=20):
471
+ def _print_parsed_data_rich(data, max_rows: int = 20) -> None:
338
472
  """Pretty print parsed data using Rich library."""
339
473
  try:
340
474
  from rich.console import Console
341
475
  from rich.table import Table
342
476
  except ImportError:
343
477
  print("⚠️ Rich library not available, falling back to text format")
344
- _print_parsed_data(data, max_rows)
478
+ _print_parsed_data_text(data, max_rows)
345
479
  return
346
480
 
347
481
  if not data:
@@ -349,8 +483,7 @@ def _print_parsed_data_rich(data, max_rows=20):
349
483
  return
350
484
 
351
485
  if not isinstance(data, list):
352
- import json
353
- print(json.dumps(data, indent=2))
486
+ print(json.dumps(data, indent=2, default=str))
354
487
  return
355
488
 
356
489
  # Get all unique keys
@@ -362,19 +495,16 @@ def _print_parsed_data_rich(data, max_rows=20):
362
495
  keys = sorted(all_keys)
363
496
 
364
497
  if not keys:
365
- import json
366
- print(json.dumps(data, indent=2))
498
+ print(json.dumps(data, indent=2, default=str))
367
499
  return
368
500
 
369
501
  # Create rich table
370
502
  console = Console()
371
503
  table = Table(show_header=True, header_style="bold cyan")
372
504
 
373
- # Add columns
374
505
  for key in keys:
375
506
  table.add_column(key, style="white", no_wrap=False, max_width=30)
376
507
 
377
- # Add rows
378
508
  shown = 0
379
509
  for row in data:
380
510
  if not isinstance(row, dict):
@@ -391,28 +521,16 @@ def _print_parsed_data_rich(data, max_rows=20):
391
521
  console.print(table)
392
522
 
393
523
 
394
- def _print_parsed_data_json(data):
395
- """Print parsed data as JSON."""
396
- import json
397
- print(json.dumps(data, indent=2))
398
-
399
-
400
- def _display_parsed_result(data, output_format, max_rows=20):
401
- """Display parsed data in the specified format."""
402
- if output_format == "json":
403
- _print_parsed_data_json(data)
404
- elif output_format == "rich":
405
- _print_parsed_data_rich(data, max_rows)
406
- else: # text or fallback
407
- _print_parsed_data(data, max_rows)
408
-
409
-
410
524
  # Convenience function to add to API
411
- def add_repl_to_api(api_instance):
525
+ def add_repl_to_api(api_instance: NTermAPI) -> None:
412
526
  """Add repl() method to API instance."""
413
527
 
414
528
  def repl(policy: Optional[REPLPolicy] = None):
415
529
  """Start interactive REPL."""
416
530
  start_repl(api=api_instance, policy=policy)
417
531
 
418
- api_instance.repl = repl
532
+ api_instance.repl = repl
533
+
534
+
535
+ # Type hint for display function
536
+ from typing import Dict