glaip-sdk 0.5.0__py3-none-any.whl → 0.5.1__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.
glaip_sdk/cli/auth.py CHANGED
@@ -109,6 +109,24 @@ def _get_token_value(prompt_for_secrets: bool, placeholder: str, console: Consol
109
109
  return placeholder
110
110
 
111
111
 
112
+ def _normalize_header_keys(
113
+ header_keys: Iterable[str] | str | None,
114
+ *,
115
+ default: Iterable[str] | None = None,
116
+ ) -> list[str]:
117
+ """Normalize header_keys to a list, handling strings and None safely."""
118
+ if header_keys is None:
119
+ return list(default or [])
120
+ if isinstance(header_keys, str):
121
+ return [header_keys] if header_keys else list(default or [])
122
+ try:
123
+ return list(header_keys)
124
+ except TypeError:
125
+ raise click.ClickException(
126
+ f"Invalid header_keys type: expected string or iterable, got {type(header_keys).__name__}"
127
+ ) from None
128
+
129
+
112
130
  def _build_bearer_headers(auth: dict[str, Any], token_value: str) -> dict[str, str]:
113
131
  """Build headers for bearer token authentication.
114
132
 
@@ -120,7 +138,10 @@ def _build_bearer_headers(auth: dict[str, Any], token_value: str) -> dict[str, s
120
138
  A dictionary of HTTP headers including the Authorization header when
121
139
  applicable.
122
140
  """
123
- header_keys = auth.get("header_keys", ["Authorization"])
141
+ default_header_keys = ["Authorization"]
142
+ has_header_keys = "header_keys" in auth
143
+ header_keys_raw = auth.get("header_keys") if has_header_keys else default_header_keys
144
+ header_keys = _normalize_header_keys(header_keys_raw, default=None if has_header_keys else default_header_keys)
124
145
  headers = {}
125
146
  for key in header_keys:
126
147
  # Prepend "Bearer " if this is Authorization header
@@ -179,8 +200,8 @@ def _extract_api_key_name(auth: dict[str, Any]) -> str | None:
179
200
  """
180
201
  key_name = auth.get("key")
181
202
  if not key_name and "header_keys" in auth:
182
- header_keys = auth["header_keys"]
183
- if isinstance(header_keys, list) and header_keys:
203
+ header_keys = _normalize_header_keys(auth["header_keys"])
204
+ if header_keys:
184
205
  key_name = header_keys[0]
185
206
  return key_name
186
207
 
@@ -228,8 +249,12 @@ def _build_api_key_headers(auth: dict[str, Any], key_name: str | None, key_value
228
249
  Returns:
229
250
  A dictionary of HTTP headers for API key authentication.
230
251
  """
231
- header_keys = [k for k in auth.get("header_keys", [key_name] if key_name else []) if k]
232
- return dict.fromkeys(header_keys, key_value)
252
+ default_header_keys = [key_name] if key_name else []
253
+ has_header_keys = "header_keys" in auth
254
+ header_keys_raw = auth.get("header_keys") if has_header_keys else default_header_keys
255
+ header_keys_list = _normalize_header_keys(header_keys_raw, default=None if has_header_keys else default_header_keys)
256
+ filtered_keys = [k for k in header_keys_list if k]
257
+ return dict.fromkeys(filtered_keys, key_value)
233
258
 
234
259
 
235
260
  def _prepare_api_key_auth(
@@ -315,9 +340,7 @@ def _extract_header_names(existing_headers: Mapping[str, Any] | None, header_key
315
340
  """
316
341
  if existing_headers:
317
342
  return list(existing_headers.keys())
318
- if header_keys:
319
- return list(header_keys)
320
- return []
343
+ return _normalize_header_keys(header_keys)
321
344
 
322
345
 
323
346
  def _is_valid_secret(value: Any) -> bool:
@@ -576,7 +576,10 @@ def list_agents(
576
576
  and len(agents) > 0
577
577
  )
578
578
 
579
+ # Track picker attempt so the fallback table doesn't re-open the palette
580
+ picker_attempted = False
579
581
  if interactive_enabled:
582
+ picker_attempted = True
580
583
  picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
581
584
  if picked_agent:
582
585
  _display_agent_details(ctx, client, picked_agent)
@@ -591,7 +594,9 @@ def list_agents(
591
594
  f"{ICON_AGENT} Available Agents",
592
595
  columns,
593
596
  transform_agent,
594
- skip_picker=simple or any(param is not None for param in (agent_type, framework, name, version)),
597
+ skip_picker=picker_attempted
598
+ or simple
599
+ or any(param is not None for param in (agent_type, framework, name, version)),
595
600
  use_pager=False,
596
601
  )
597
602
 
glaip_sdk/cli/utils.py CHANGED
@@ -12,6 +12,7 @@ import importlib
12
12
  import json
13
13
  import logging
14
14
  import os
15
+ import re
15
16
  import sys
16
17
  from collections.abc import Callable, Iterable
17
18
  from contextlib import AbstractContextManager, contextmanager, nullcontext
@@ -787,56 +788,60 @@ class _FuzzyCompleter:
787
788
  self.words = words
788
789
 
789
790
  def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
790
- """Get fuzzy completions for the current word.
791
+ """Get fuzzy completions for the current word, ranked by score.
791
792
 
792
793
  Args:
793
794
  document: Document object from prompt_toolkit.
794
795
  _complete_event: Completion event (unused).
795
796
 
796
797
  Yields:
797
- Completion objects matching the current word.
798
+ Completion objects matching the current word, in ranked order.
798
799
  """
799
- word = document.get_word_before_cursor()
800
- if not word:
800
+ # Get the entire buffer text (not just word before cursor)
801
+ buffer_text = document.text_before_cursor
802
+ if not buffer_text or not isinstance(buffer_text, str):
801
803
  return
802
804
 
803
- word_lower = word.lower()
804
- for label in self.words:
805
- label_lower = label.lower()
806
- if self._fuzzy_match(word_lower, label_lower):
807
- yield Completion(label, start_position=-len(word))
808
-
809
- def _fuzzy_match(self, search: str, target: str) -> bool: # pragma: no cover
810
- """True fuzzy matching: checks if all characters in search appear in order in target."""
811
- if not search:
812
- return True
813
-
814
- search_idx = 0
815
- for char in target:
816
- if search_idx < len(search) and search[search_idx] == char:
817
- search_idx += 1
818
- if search_idx == len(search):
819
- return True
820
- return False
805
+ # Rank labels by fuzzy score
806
+ ranked_labels = _rank_labels(self.words, buffer_text)
807
+
808
+ # Yield ranked completions
809
+ for label in ranked_labels:
810
+ # Replace entire buffer text, not just the word before cursor
811
+ # This prevents concatenation issues with hyphenated names
812
+ yield Completion(label, start_position=-len(buffer_text))
821
813
 
822
814
 
823
815
  def _perform_fuzzy_search(answer: str, labels: list[str], by_label: dict[str, dict[str, Any]]) -> dict[str, Any] | None:
824
- """Perform fuzzy search fallback and return best match."""
816
+ """Perform fuzzy search fallback and return best match.
817
+
818
+ Returns:
819
+ Selected resource dict or None if cancelled/no match.
820
+ """
825
821
  # Exact label match
826
822
  if answer in by_label:
827
823
  return by_label[answer]
828
824
 
829
- # Fuzzy search fallback
830
- best_match = None
831
- best_score = -1
832
-
825
+ # Fuzzy search fallback using ranked labels
826
+ # Check if query actually matches anything before ranking
827
+ query_lower = answer.lower()
828
+ has_match = False
833
829
  for label in labels:
834
- score = _fuzzy_score(answer.lower(), label.lower())
835
- if score > best_score:
836
- best_score = score
837
- best_match = label
830
+ if _fuzzy_score(query_lower, label.lower()) >= 0:
831
+ has_match = True
832
+ break
838
833
 
839
- return by_label[best_match] if best_match and best_score > 0 else None
834
+ if not has_match:
835
+ return None
836
+
837
+ ranked_labels = _rank_labels(labels, answer)
838
+ if ranked_labels:
839
+ # Return the top-ranked match
840
+ best_match = ranked_labels[0]
841
+ if best_match in by_label:
842
+ return by_label[best_match]
843
+
844
+ return None
840
845
 
841
846
 
842
847
  def _fuzzy_pick(
@@ -865,33 +870,61 @@ def _fuzzy_pick(
865
870
  return _perform_fuzzy_search(answer, labels, by_label) if answer else None
866
871
 
867
872
 
868
- def _is_fuzzy_match(search: str, target: str) -> bool:
869
- """Check if search string is a fuzzy match for target."""
873
+ def _strip_spaces_for_matching(value: str) -> str:
874
+ """Remove whitespace from a query for consistent fuzzy matching."""
875
+ return re.sub(r"\s+", "", value)
876
+
877
+
878
+ def _is_fuzzy_match(search: Any, target: str) -> bool:
879
+ """Case-insensitive fuzzy match with optional spaces."""
880
+ # Ensure search is a string
881
+ if not isinstance(search, str):
882
+ return False
883
+
870
884
  if not search:
871
885
  return True
872
886
 
887
+ # Strip spaces from search query - treat them as optional separators
888
+ # This allows "test agent" to match "test-agent", "test_agent", etc.
889
+ search_no_spaces = _strip_spaces_for_matching(search).lower()
890
+ if not search_no_spaces:
891
+ # If search is only spaces, match everything
892
+ return True
893
+
873
894
  search_idx = 0
874
- for char in target:
875
- if search_idx < len(search) and search[search_idx] == char:
895
+ for char in target.lower():
896
+ if search_idx < len(search_no_spaces) and search_no_spaces[search_idx] == char:
876
897
  search_idx += 1
877
- if search_idx == len(search):
898
+ if search_idx == len(search_no_spaces):
878
899
  return True
879
900
  return False
880
901
 
881
902
 
882
903
  def _calculate_exact_match_bonus(search: str, target: str) -> int:
883
- """Calculate bonus for exact substring matches."""
884
- return 100 if search.lower() in target.lower() else 0
904
+ """Calculate bonus for exact substring matches.
905
+
906
+ Spaces in search are treated as optional separators (stripped before matching).
907
+ """
908
+ # Strip spaces from search - treat them as optional separators
909
+ search_no_spaces = _strip_spaces_for_matching(search).lower()
910
+ if not search_no_spaces:
911
+ return 0
912
+ return 100 if search_no_spaces in target.lower() else 0
885
913
 
886
914
 
887
915
  def _calculate_consecutive_bonus(search: str, target: str) -> int:
888
- """Calculate bonus for consecutive character matches."""
916
+ """Case-insensitive consecutive-character bonus."""
917
+ # Strip spaces from search - treat them as optional separators
918
+ search_no_spaces = _strip_spaces_for_matching(search).lower()
919
+ if not search_no_spaces:
920
+ return 0
921
+
889
922
  consecutive = 0
890
923
  max_consecutive = 0
891
924
  search_idx = 0
892
925
 
893
- for char in target:
894
- if search_idx < len(search) and search[search_idx] == char:
926
+ for char in target.lower():
927
+ if search_idx < len(search_no_spaces) and search_no_spaces[search_idx] == char:
895
928
  consecutive += 1
896
929
  max_consecutive = max(max_consecutive, consecutive)
897
930
  search_idx += 1
@@ -902,16 +935,31 @@ def _calculate_consecutive_bonus(search: str, target: str) -> int:
902
935
 
903
936
 
904
937
  def _calculate_length_bonus(search: str, target: str) -> int:
905
- """Calculate bonus for shorter search terms."""
906
- return (len(target) - len(search)) * 2
938
+ """Calculate bonus for shorter search terms.
939
+
940
+ Spaces in search are treated as optional separators (stripped before calculation).
941
+ """
942
+ # Strip spaces from search - treat them as optional separators
943
+ search_no_spaces = _strip_spaces_for_matching(search)
944
+ if not search_no_spaces:
945
+ return 0
946
+ return max(0, (len(target) - len(search_no_spaces)) * 2)
907
947
 
908
948
 
909
- def _fuzzy_score(search: str, target: str) -> int:
949
+ def _fuzzy_score(search: Any, target: str) -> int:
910
950
  """Calculate fuzzy match score.
911
951
 
912
952
  Higher score = better match.
913
953
  Returns -1 if no match possible.
954
+
955
+ Args:
956
+ search: Search string (or any type - non-strings return -1)
957
+ target: Target string to match against
914
958
  """
959
+ # Ensure search is a string first
960
+ if not isinstance(search, str):
961
+ return -1
962
+
915
963
  if not search:
916
964
  return 0
917
965
 
@@ -927,6 +975,59 @@ def _fuzzy_score(search: str, target: str) -> int:
927
975
  return score
928
976
 
929
977
 
978
+ def _extract_id_suffix(label: str) -> str:
979
+ """Extract ID suffix from label for tie-breaking.
980
+
981
+ Args:
982
+ label: Display label (e.g., "name • [abc123...]")
983
+
984
+ Returns:
985
+ ID suffix string (e.g., "abc123") or empty string if not found
986
+ """
987
+ # Look for pattern like "[abc123...]" or "[abc123]"
988
+ match = re.search(r"\[([^\]]+)\]", label)
989
+ return match.group(1) if match else ""
990
+
991
+
992
+ def _rank_labels(labels: list[str], query: Any) -> list[str]:
993
+ """Rank labels by fuzzy score with deterministic tie-breaks.
994
+
995
+ Args:
996
+ labels: List of display labels to rank
997
+ query: Search query string (or any type - non-strings return sorted labels)
998
+
999
+ Returns:
1000
+ Labels sorted by fuzzy score (descending), then case-insensitive label,
1001
+ then id suffix for deterministic ordering.
1002
+ """
1003
+ if not query:
1004
+ # No query: sort by case-insensitive label, then id suffix
1005
+ return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1006
+
1007
+ # Ensure query is a string
1008
+ if not isinstance(query, str):
1009
+ return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1010
+
1011
+ query_lower = query.lower()
1012
+
1013
+ # Calculate scores and create tuples for sorting
1014
+ scored_labels = []
1015
+ for label in labels:
1016
+ label_lower = label.lower()
1017
+ score = _fuzzy_score(query_lower, label_lower)
1018
+ if score >= 0: # Only include matches
1019
+ scored_labels.append((score, label_lower, _extract_id_suffix(label), label))
1020
+
1021
+ if not scored_labels:
1022
+ # No fuzzy matches: fall back to deterministic label sorting
1023
+ return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1024
+
1025
+ # Sort by: score (desc), label (case-insensitive), id suffix, original label
1026
+ scored_labels.sort(key=lambda x: (-x[0], x[1], x[2], x[3]))
1027
+
1028
+ return [label for _score, _label_lower, _id_suffix, label in scored_labels]
1029
+
1030
+
930
1031
  # ----------------------- Structured renderer helpers -------------------- #
931
1032
 
932
1033
 
@@ -1172,7 +1273,12 @@ def _print_selection_tip(title: str) -> None:
1172
1273
 
1173
1274
 
1174
1275
  def _handle_fuzzy_pick_selection(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> bool:
1175
- """Handle fuzzy picker selection, returns True if selection was made."""
1276
+ """Handle fuzzy picker selection.
1277
+
1278
+ Returns:
1279
+ True if a resource was selected and displayed,
1280
+ False if cancelled/no selection.
1281
+ """
1176
1282
  picked = _try_fuzzy_pick(rows, columns, title)
1177
1283
  if picked is None:
1178
1284
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: glaip-sdk
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
5
5
  License: MIT
6
6
  Author: Raymond Christopher
@@ -4,10 +4,10 @@ glaip_sdk/branding.py,sha256=tLqYCIHMkUf8p2smpuAGNptwaKUN38G4mlh0A0DOl_w,7823
4
4
  glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
5
5
  glaip_sdk/cli/account_store.py,sha256=NXuAVPaJS_32Aw1VTaZCNwIID-gADw4F_UMieoWmg3g,17336
6
6
  glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
7
- glaip_sdk/cli/auth.py,sha256=ycT6CepbJDfUqRT0KBYXfBhnJYd-zJ_np1-DSiY3EWs,23040
7
+ glaip_sdk/cli/auth.py,sha256=9hfjZyd4cx2_mImqykJ1sWQsuVTR2gy6D4hFqAQNKL4,24129
8
8
  glaip_sdk/cli/commands/__init__.py,sha256=6Z3ASXDut0lAbUX_umBFtxPzzFyqoiZfVeTahThFu1A,219
9
9
  glaip_sdk/cli/commands/accounts.py,sha256=VCG-JZGY86DlWO5bAfDZ70RuyKQ5q-Rh4U0iM8NwO6M,13755
10
- glaip_sdk/cli/commands/agents.py,sha256=CGYWTPpcFXUms_3SMB5Mr-YRpBh7c8iu-Mj245XhSCc,47686
10
+ glaip_sdk/cli/commands/agents.py,sha256=y89okY-a5sM_QCS3F3C66DF7yhhHFbUJ7ZzIl2DUEck,47880
11
11
  glaip_sdk/cli/commands/common_config.py,sha256=IY13gPkeifXxSdpzRFUvfRin8J7s38p6Y7TYjdGw7w4,2474
12
12
  glaip_sdk/cli/commands/configure.py,sha256=8vfgtNEMK2lnEk3i6H1ZevsjxnYA6jAj4evhWmsHi6w,14494
13
13
  glaip_sdk/cli/commands/mcps.py,sha256=tttqQnfM89iI9Pm94u8YRhiHMQNYNouecFX0brsT4cQ,42551
@@ -44,7 +44,7 @@ glaip_sdk/cli/transcript/history.py,sha256=2FBjawxP8CX9gRPMUMP8bDjG50BGM2j2zk6If
44
44
  glaip_sdk/cli/transcript/launcher.py,sha256=z5ivkPXDQJpATIqtRLUK8jH3p3WIZ72PvOPqYRDMJvw,2327
45
45
  glaip_sdk/cli/transcript/viewer.py,sha256=ar1SzRkhKIf3_DgFz1EG1RZGDmd2w2wogAe038DLL_M,13037
46
46
  glaip_sdk/cli/update_notifier.py,sha256=qv-GfwTYZdrhlSbC_71I1AvKY9cV4QVBmtees16S2Xg,9807
47
- glaip_sdk/cli/utils.py,sha256=Dtvi8mkrU3v88ZO2HwHL2iuzY8Ic3PTDL0YySWsvy0Q,53510
47
+ glaip_sdk/cli/utils.py,sha256=iPtt4xAqtCW-dwQ-JWVwoPVPAm-P1R8C-1kih6ZIYXU,57255
48
48
  glaip_sdk/cli/validators.py,sha256=d-kq4y7HWMo6Gc7wLXWUsCt8JwFvJX_roZqRm1Nko1I,5622
49
49
  glaip_sdk/client/__init__.py,sha256=F-eE_dRSzA0cc1it06oi0tZetZBHmSUjWSHGhJMLCls,263
50
50
  glaip_sdk/client/_agent_payloads.py,sha256=VfBHoijuoqUOixGBf2SA2vlQIXQmBsjB3sXHZhXYiec,17681
@@ -107,7 +107,7 @@ glaip_sdk/utils/resource_refs.py,sha256=vF34kyAtFBLnaKnQVrsr2st1JiSxVbIZ4yq0DelJ
107
107
  glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
108
108
  glaip_sdk/utils/serialization.py,sha256=z-qpvWLSBrGK3wbUclcA1UIKLXJedTnMSwPdq-FF4lo,13308
109
109
  glaip_sdk/utils/validation.py,sha256=Vt8oSnn7OM6ns5vjOl5FwGIMWBPb0yI6RD5XL_L5_4M,6826
110
- glaip_sdk-0.5.0.dist-info/METADATA,sha256=869wDTHn4xaN_DBTqmlZG7bQ7UXzKuhqGp1Mx_umkV8,7053
111
- glaip_sdk-0.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
112
- glaip_sdk-0.5.0.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
113
- glaip_sdk-0.5.0.dist-info/RECORD,,
110
+ glaip_sdk-0.5.1.dist-info/METADATA,sha256=jxEyfPZqz2g7nLHnFidlNnMPkljgrLyKVYk3qVnThLE,7053
111
+ glaip_sdk-0.5.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
112
+ glaip_sdk-0.5.1.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
113
+ glaip_sdk-0.5.1.dist-info/RECORD,,