glaip-sdk 0.3.0__py3-none-any.whl → 0.4.0__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.
Files changed (50) hide show
  1. glaip_sdk/cli/auth.py +2 -1
  2. glaip_sdk/cli/commands/agents.py +1 -1
  3. glaip_sdk/cli/commands/configure.py +2 -1
  4. glaip_sdk/cli/commands/mcps.py +191 -44
  5. glaip_sdk/cli/commands/transcripts.py +1 -1
  6. glaip_sdk/cli/display.py +1 -1
  7. glaip_sdk/cli/hints.py +58 -0
  8. glaip_sdk/cli/io.py +6 -3
  9. glaip_sdk/cli/main.py +2 -1
  10. glaip_sdk/cli/slash/agent_session.py +2 -1
  11. glaip_sdk/cli/slash/session.py +1 -1
  12. glaip_sdk/cli/transcript/capture.py +1 -1
  13. glaip_sdk/cli/transcript/viewer.py +13 -646
  14. glaip_sdk/cli/update_notifier.py +2 -1
  15. glaip_sdk/cli/utils.py +63 -110
  16. glaip_sdk/client/agents.py +2 -4
  17. glaip_sdk/client/main.py +2 -18
  18. glaip_sdk/client/mcps.py +11 -1
  19. glaip_sdk/client/run_rendering.py +90 -111
  20. glaip_sdk/client/shared.py +21 -0
  21. glaip_sdk/models.py +8 -7
  22. glaip_sdk/utils/display.py +23 -15
  23. glaip_sdk/utils/rendering/__init__.py +6 -13
  24. glaip_sdk/utils/rendering/formatting.py +5 -30
  25. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  26. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  27. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  28. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  29. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  30. glaip_sdk/utils/rendering/models.py +1 -0
  31. glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
  32. glaip_sdk/utils/rendering/renderer/base.py +214 -1469
  33. glaip_sdk/utils/rendering/renderer/debug.py +24 -0
  34. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  35. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  36. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  37. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  38. glaip_sdk/utils/rendering/state.py +204 -0
  39. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  40. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  41. glaip_sdk/utils/rendering/steps/format.py +176 -0
  42. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  43. glaip_sdk/utils/rendering/timing.py +36 -0
  44. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  45. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  46. glaip_sdk/utils/validation.py +13 -21
  47. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/METADATA +1 -1
  48. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/RECORD +50 -34
  49. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/auth.py CHANGED
@@ -18,7 +18,8 @@ import click
18
18
  from rich.console import Console
19
19
 
20
20
  from glaip_sdk.branding import HINT_PREFIX_STYLE, WARNING_STYLE
21
- from glaip_sdk.cli.utils import command_hint, format_command_hint
21
+ from glaip_sdk.cli.hints import format_command_hint
22
+ from glaip_sdk.cli.utils import command_hint
22
23
 
23
24
 
24
25
  def prepare_authentication_export(
@@ -59,13 +59,13 @@ from glaip_sdk.cli.transcript import (
59
59
  maybe_launch_post_run_viewer,
60
60
  store_transcript_for_session,
61
61
  )
62
+ from glaip_sdk.cli.hints import in_slash_mode
62
63
  from glaip_sdk.cli.utils import (
63
64
  _fuzzy_pick_for_resources,
64
65
  build_renderer,
65
66
  coerce_to_row,
66
67
  get_client,
67
68
  handle_resource_export,
68
- in_slash_mode,
69
69
  output_list,
70
70
  output_result,
71
71
  spinner_context,
@@ -23,7 +23,8 @@ from glaip_sdk.branding import (
23
23
  )
24
24
  from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
25
25
  from glaip_sdk.cli.rich_helpers import markup_text
26
- from glaip_sdk.cli.utils import command_hint, format_command_hint, sdk_version
26
+ from glaip_sdk.cli.hints import format_command_hint
27
+ from glaip_sdk.cli.utils import command_hint, sdk_version
27
28
  from glaip_sdk.icons import ICON_TOOL
28
29
  from glaip_sdk.rich_components import AIPTable
29
30
 
@@ -63,6 +63,7 @@ from glaip_sdk.utils.serialization import (
63
63
  )
64
64
 
65
65
  console = Console()
66
+ MAX_DESCRIPTION_LEN = 50
66
67
 
67
68
 
68
69
  def _is_sensitive_data(val: Any) -> bool:
@@ -879,64 +880,210 @@ def get(
879
880
  raise click.ClickException(str(e)) from e
880
881
 
881
882
 
883
+ def _get_tools_from_config(ctx: Any, client: Any, config_file: str) -> tuple[list[dict[str, Any]], str]:
884
+ """Get tools from MCP config file.
885
+
886
+ Args:
887
+ ctx: Click context
888
+ client: GlaIP client instance
889
+ config_file: Path to config file
890
+
891
+ Returns:
892
+ Tuple of (tools list, title string)
893
+ """
894
+ config_data = load_resource_from_file_with_validation(Path(config_file), "MCP config")
895
+
896
+ # Validate config structure
897
+ transport = config_data.get("transport")
898
+ if "config" not in config_data:
899
+ raise click.ClickException("Invalid MCP config: missing 'config' section in the file.")
900
+ config_data["config"] = validate_mcp_config_structure(
901
+ config_data["config"],
902
+ transport=transport,
903
+ source=config_file,
904
+ )
905
+
906
+ # Get tools from config without saving
907
+ with spinner_context(
908
+ ctx,
909
+ "[bold blue]Fetching tools from config…[/bold blue]",
910
+ console_override=console,
911
+ ):
912
+ tools = client.mcps.get_mcp_tools_from_config(config_data)
913
+
914
+ title = f"{ICON_TOOL} Tools from config: {Path(config_file).name}"
915
+ return tools, title
916
+
917
+
918
+ def _get_tools_from_mcp(ctx: Any, client: Any, mcp_ref: str | None) -> tuple[list[dict[str, Any]], str]:
919
+ """Get tools from saved MCP.
920
+
921
+ Args:
922
+ ctx: Click context
923
+ client: GlaIP client instance
924
+ mcp_ref: MCP reference (ID or name)
925
+
926
+ Returns:
927
+ Tuple of (tools list, title string)
928
+ """
929
+ mcp = _resolve_mcp(ctx, client, mcp_ref)
930
+
931
+ with spinner_context(
932
+ ctx,
933
+ "[bold blue]Fetching MCP tools…[/bold blue]",
934
+ console_override=console,
935
+ ):
936
+ tools = client.mcps.get_mcp_tools(mcp.id)
937
+
938
+ title = f"{ICON_TOOL} Tools from MCP: {mcp.name}"
939
+ return tools, title
940
+
941
+
942
+ def _output_tool_names(ctx: Any, tools: list[dict[str, Any]]) -> None:
943
+ """Output only tool names.
944
+
945
+ Args:
946
+ ctx: Click context
947
+ tools: List of tool dictionaries
948
+ """
949
+ view = get_ctx_value(ctx, "view", "rich")
950
+ tool_names = [tool.get("name", "N/A") for tool in tools]
951
+
952
+ if view == "json":
953
+ handle_json_output(ctx, tool_names)
954
+ elif view == "plain":
955
+ if tool_names:
956
+ for name in tool_names:
957
+ console.print(name, markup=False)
958
+ console.print(f"Total: {len(tool_names)} tools", markup=False)
959
+ else:
960
+ console.print("No tools found", markup=False)
961
+ else:
962
+ if tool_names:
963
+ for name in tool_names:
964
+ console.print(name)
965
+ console.print(f"[dim]Total: {len(tool_names)} tools[/dim]")
966
+ else:
967
+ console.print("[yellow]No tools found[/yellow]")
968
+
969
+
970
+ def _transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
971
+ """Transform a tool dictionary to a display row dictionary.
972
+
973
+ Args:
974
+ tool: Tool dictionary to transform.
975
+
976
+ Returns:
977
+ Dictionary with name and description fields.
978
+ """
979
+ description = tool.get("description", "N/A")
980
+ if len(description) > MAX_DESCRIPTION_LEN:
981
+ description = description[: MAX_DESCRIPTION_LEN - 3] + "..."
982
+ return {
983
+ "name": tool.get("name", "N/A"),
984
+ "description": description,
985
+ }
986
+
987
+
988
+ def _output_tools_table(ctx: Any, tools: list[dict[str, Any]], title: str) -> None:
989
+ """Output tools in table format.
990
+
991
+ Args:
992
+ ctx: Click context
993
+ tools: List of tool dictionaries
994
+ title: Table title
995
+ """
996
+ columns = [
997
+ ("name", "Name", ACCENT_STYLE, None),
998
+ ("description", "Description", INFO, 50),
999
+ ]
1000
+
1001
+ output_list(
1002
+ ctx,
1003
+ tools,
1004
+ title,
1005
+ columns,
1006
+ _transform_tool,
1007
+ )
1008
+
1009
+
1010
+ def _validate_tool_command_args(mcp_ref: str | None, config_file: str | None) -> None:
1011
+ """Validate that exactly one of mcp_ref or config_file is provided.
1012
+
1013
+ Args:
1014
+ mcp_ref: MCP reference (ID or name)
1015
+ config_file: Path to config file
1016
+
1017
+ Raises:
1018
+ ClickException: If validation fails
1019
+ """
1020
+ if not mcp_ref and not config_file:
1021
+ raise click.ClickException(
1022
+ "Either MCP_REF or --from-config must be provided.\n"
1023
+ "Examples:\n"
1024
+ " aip mcps tools <MCP_ID>\n"
1025
+ " aip mcps tools --from-config mcp-config.json"
1026
+ )
1027
+ if mcp_ref and config_file:
1028
+ raise click.ClickException(
1029
+ "Cannot use both MCP_REF and --from-config at the same time.\n"
1030
+ "Use either:\n"
1031
+ " aip mcps tools <MCP_ID>\n"
1032
+ " aip mcps tools --from-config mcp-config.json"
1033
+ )
1034
+
1035
+
882
1036
  @mcps_group.command("tools")
883
- @click.argument("mcp_ref")
1037
+ @click.argument("mcp_ref", required=False)
1038
+ @click.option(
1039
+ "--from-config",
1040
+ "--config",
1041
+ "config_file",
1042
+ type=click.Path(exists=True, dir_okay=False),
1043
+ help="Get tools from MCP config file without saving to DB (JSON or YAML)",
1044
+ )
1045
+ @click.option(
1046
+ "--names-only",
1047
+ is_flag=True,
1048
+ help="Show only tool names (useful for allowed_tools config)",
1049
+ )
884
1050
  @output_flags()
885
1051
  @click.pass_context
886
- def list_tools(ctx: Any, mcp_ref: str) -> None:
887
- """List tools available from a specific MCP.
1052
+ def list_tools(ctx: Any, mcp_ref: str | None, config_file: str | None, names_only: bool) -> None:
1053
+ """List tools available from a specific MCP or config file.
888
1054
 
889
1055
  Args:
890
1056
  ctx: Click context containing output format preferences
891
- mcp_ref: MCP reference (ID or name)
1057
+ mcp_ref: MCP reference (ID or name) - required if --from-config not used
1058
+ config_file: Path to MCP config file - alternative to mcp_ref
1059
+ names_only: Show only tool names instead of full table
892
1060
 
893
1061
  Raises:
894
1062
  ClickException: If MCP not found or tools fetch fails
895
- """
896
- try:
897
- client = get_client(ctx)
898
1063
 
899
- # Resolve MCP using helper function
900
- mcp = _resolve_mcp(ctx, client, mcp_ref)
901
-
902
- # Get tools from MCP
903
- with spinner_context(
904
- ctx,
905
- "[bold blue]Fetching MCP tools…[/bold blue]",
906
- console_override=console,
907
- ):
908
- tools = client.mcps.get_mcp_tools(mcp.id)
909
-
910
- # Define table columns: (data_key, header, style, width)
911
- columns = [
912
- ("name", "Name", ACCENT_STYLE, None),
913
- ("description", "Description", INFO, 50),
914
- ]
1064
+ Examples:
1065
+ Get tools from saved MCP:
1066
+ aip mcps tools <MCP_ID>
915
1067
 
916
- # Transform function for safe dictionary access
917
- def transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
918
- """Transform a tool dictionary to a display row dictionary.
1068
+ Get tools from config file (without saving to DB):
1069
+ aip mcps tools --from-config mcp-config.json
919
1070
 
920
- Args:
921
- tool: Tool dictionary to transform.
1071
+ Get just tool names for allowed_tools config:
1072
+ aip mcps tools <MCP_ID> --names-only
1073
+ """
1074
+ try:
1075
+ _validate_tool_command_args(mcp_ref, config_file)
1076
+ client = get_client(ctx)
922
1077
 
923
- Returns:
924
- Dictionary with name and description fields.
925
- """
926
- return {
927
- "name": tool.get("name", "N/A"),
928
- "description": tool.get("description", "N/A")[:47] + "..."
929
- if len(tool.get("description", "")) > 47
930
- else tool.get("description", "N/A"),
931
- }
1078
+ if config_file:
1079
+ tools, title = _get_tools_from_config(ctx, client, config_file)
1080
+ else:
1081
+ tools, title = _get_tools_from_mcp(ctx, client, mcp_ref)
932
1082
 
933
- output_list(
934
- ctx,
935
- tools,
936
- f"{ICON_TOOL} Tools from MCP: {mcp.name}",
937
- columns,
938
- transform_tool,
939
- )
1083
+ if names_only:
1084
+ _output_tool_names(ctx, tools)
1085
+ else:
1086
+ _output_tools_table(ctx, tools, title)
940
1087
 
941
1088
  except Exception as e:
942
1089
  raise click.ClickException(str(e)) from e
@@ -37,7 +37,7 @@ from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
37
37
  from glaip_sdk.cli.utils import format_size, get_ctx_value, parse_json_line
38
38
  from glaip_sdk.rich_components import AIPTable
39
39
  from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
40
- from glaip_sdk.utils.rendering.renderer.panels import create_final_panel
40
+ from glaip_sdk.utils.rendering.layout.panels import create_final_panel
41
41
 
42
42
  console = Console()
43
43
 
glaip_sdk/cli/display.py CHANGED
@@ -17,7 +17,7 @@ from rich.text import Text
17
17
 
18
18
  from glaip_sdk.branding import ERROR_STYLE, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
19
19
  from glaip_sdk.cli.rich_helpers import markup_text
20
- from glaip_sdk.cli.utils import command_hint, format_command_hint, in_slash_mode
20
+ from glaip_sdk.cli.hints import command_hint, format_command_hint, in_slash_mode
21
21
  from glaip_sdk.icons import ICON_AGENT, ICON_TOOL
22
22
  from glaip_sdk.rich_components import AIPPanel
23
23
 
glaip_sdk/cli/hints.py ADDED
@@ -0,0 +1,58 @@
1
+ """Helpers for formatting CLI/slash command hints.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ import click
11
+
12
+ from glaip_sdk.branding import HINT_COMMAND_STYLE, HINT_DESCRIPTION_COLOR
13
+
14
+
15
+ def in_slash_mode(ctx: click.Context | None = None) -> bool:
16
+ """Return True when running inside the slash command palette."""
17
+ if ctx is None:
18
+ try:
19
+ ctx = click.get_current_context(silent=True)
20
+ except RuntimeError:
21
+ ctx = None
22
+
23
+ if ctx is None:
24
+ return False
25
+
26
+ obj = getattr(ctx, "obj", None)
27
+ if isinstance(obj, dict):
28
+ return bool(obj.get("_slash_session"))
29
+
30
+ return bool(getattr(obj, "_slash_session", False))
31
+
32
+
33
+ def command_hint(
34
+ cli_command: str | None,
35
+ slash_command: str | None = None,
36
+ *,
37
+ ctx: click.Context | None = None,
38
+ ) -> str | None:
39
+ """Return the appropriate command string for the current mode."""
40
+ if in_slash_mode(ctx):
41
+ if not slash_command:
42
+ return None
43
+ return slash_command if slash_command.startswith("/") else f"/{slash_command}"
44
+
45
+ if not cli_command:
46
+ return None
47
+ return f"aip {cli_command}"
48
+
49
+
50
+ def format_command_hint(command: str | None, description: str | None = None) -> str | None:
51
+ """Return a Rich markup string that highlights a command hint."""
52
+ if not command:
53
+ return None
54
+
55
+ highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
56
+ if description:
57
+ highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
58
+ return highlighted
glaip_sdk/cli/io.py CHANGED
@@ -7,6 +7,7 @@ Authors:
7
7
  Raymond Christopher (raymond.christopher@gdplabs.id)
8
8
  """
9
9
 
10
+ from importlib import import_module
10
11
  from pathlib import Path
11
12
  from typing import TYPE_CHECKING, Any
12
13
 
@@ -25,9 +26,11 @@ if TYPE_CHECKING: # pragma: no cover - typing-only imports
25
26
 
26
27
  def _create_console() -> "Console":
27
28
  """Return a Console instance (lazy import for easier testing)."""
28
- from rich.console import Console # Local import for test patching
29
-
30
- return Console()
29
+ try:
30
+ console_module = import_module("rich.console")
31
+ except ImportError as exc: # pragma: no cover - optional dependency missing
32
+ raise RuntimeError("Rich Console is not available") from exc
33
+ return console_module.Console()
31
34
 
32
35
 
33
36
  def load_resource_from_file_with_validation(file_path: Path, resource_type: str) -> dict[str, Any]:
glaip_sdk/cli/main.py CHANGED
@@ -38,7 +38,8 @@ from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
38
38
  from glaip_sdk.cli.config import load_config
39
39
  from glaip_sdk.cli.transcript import get_transcript_cache_stats
40
40
  from glaip_sdk.cli.update_notifier import maybe_notify_update
41
- from glaip_sdk.cli.utils import format_size, in_slash_mode, sdk_version, spinner_context, update_spinner
41
+ from glaip_sdk.cli.hints import in_slash_mode
42
+ from glaip_sdk.cli.utils import format_size, sdk_version, spinner_context, update_spinner
42
43
  from glaip_sdk.config.constants import (
43
44
  DEFAULT_AGENT_RUN_TIMEOUT,
44
45
  )
@@ -16,7 +16,8 @@ from glaip_sdk.cli.commands.agents import get as agents_get_command
16
16
  from glaip_sdk.cli.commands.agents import run as agents_run_command
17
17
  from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
18
18
  from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
19
- from glaip_sdk.cli.utils import bind_slash_session_context, format_command_hint
19
+ from glaip_sdk.cli.hints import format_command_hint
20
+ from glaip_sdk.cli.utils import bind_slash_session_context
20
21
 
21
22
  if TYPE_CHECKING: # pragma: no cover - type checking only
22
23
  from glaip_sdk.cli.slash.session import SlashSession
@@ -52,10 +52,10 @@ from glaip_sdk.cli.transcript import (
52
52
  )
53
53
  from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
54
54
  from glaip_sdk.cli.update_notifier import maybe_notify_update
55
+ from glaip_sdk.cli.hints import format_command_hint
55
56
  from glaip_sdk.cli.utils import (
56
57
  _fuzzy_pick_for_resources,
57
58
  command_hint,
58
- format_command_hint,
59
59
  format_size,
60
60
  get_client,
61
61
  restore_slash_session_context,
@@ -24,7 +24,7 @@ from glaip_sdk.cli.transcript.cache import (
24
24
  from glaip_sdk.cli.transcript.cache import (
25
25
  build_payload as build_transcript_payload,
26
26
  )
27
- from glaip_sdk.utils.rendering.renderer.progress import format_tool_title
27
+ from glaip_sdk.utils.rendering.layout.progress import format_tool_title
28
28
 
29
29
 
30
30
  @dataclass(slots=True)