glaip-sdk 0.5.0__py3-none-any.whl → 0.5.2__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 +31 -8
- glaip_sdk/cli/commands/accounts.py +158 -13
- glaip_sdk/cli/commands/agents.py +9 -1
- glaip_sdk/cli/utils.py +154 -46
- {glaip_sdk-0.5.0.dist-info → glaip_sdk-0.5.2.dist-info}/METADATA +1 -1
- {glaip_sdk-0.5.0.dist-info → glaip_sdk-0.5.2.dist-info}/RECORD +8 -8
- {glaip_sdk-0.5.0.dist-info → glaip_sdk-0.5.2.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.5.0.dist-info → glaip_sdk-0.5.2.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -106,6 +106,9 @@ def list_accounts(output_json: bool) -> None:
|
|
|
106
106
|
if active_account:
|
|
107
107
|
console.print(f"\n[{SUCCESS_STYLE}]Active account[/]: {active_account}")
|
|
108
108
|
|
|
109
|
+
# Show hint for updating accounts
|
|
110
|
+
console.print(f"\n[{INFO}]💡 Tip[/]: To update an account's URL or key, use: [bold]aip accounts edit <name>[/bold]")
|
|
111
|
+
|
|
109
112
|
|
|
110
113
|
def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) -> dict[str, str] | None:
|
|
111
114
|
"""Check if account exists and handle overwrite logic.
|
|
@@ -128,13 +131,19 @@ def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) ->
|
|
|
128
131
|
return existing
|
|
129
132
|
|
|
130
133
|
|
|
131
|
-
def _get_credentials_non_interactive(
|
|
134
|
+
def _get_credentials_non_interactive(
|
|
135
|
+
url: str,
|
|
136
|
+
read_key_from_stdin: bool,
|
|
137
|
+
name: str,
|
|
138
|
+
command_name: str = "aip accounts add",
|
|
139
|
+
) -> tuple[str, str]:
|
|
132
140
|
"""Get credentials in non-interactive mode.
|
|
133
141
|
|
|
134
142
|
Args:
|
|
135
143
|
url: API URL from flag.
|
|
136
144
|
read_key_from_stdin: Whether to read key from stdin.
|
|
137
145
|
name: Account name (for error messages).
|
|
146
|
+
command_name: Command name for guidance text.
|
|
138
147
|
|
|
139
148
|
Returns:
|
|
140
149
|
Tuple of (api_url, api_key).
|
|
@@ -147,7 +156,7 @@ def _get_credentials_non_interactive(url: str, read_key_from_stdin: bool, name:
|
|
|
147
156
|
return url, sys.stdin.read().strip()
|
|
148
157
|
console.print(
|
|
149
158
|
f"[{ERROR_STYLE}]Error: --key requires stdin input. "
|
|
150
|
-
f"Use: cat key.txt |
|
|
159
|
+
f"Use: cat key.txt | {command_name} {name} --url {url} --key[/]",
|
|
151
160
|
)
|
|
152
161
|
raise click.Abort()
|
|
153
162
|
# URL provided, prompt for key
|
|
@@ -178,6 +187,88 @@ def _get_credentials_interactive(read_key_from_stdin: bool, existing: dict[str,
|
|
|
178
187
|
return _prompt_account_inputs(existing)
|
|
179
188
|
|
|
180
189
|
|
|
190
|
+
def _handle_key_rotation(
|
|
191
|
+
name: str,
|
|
192
|
+
existing_url: str,
|
|
193
|
+
command_name: str,
|
|
194
|
+
) -> tuple[str, str]:
|
|
195
|
+
"""Handle key rotation using stored URL.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
name: Account name (for error messages).
|
|
199
|
+
existing_url: Existing account URL.
|
|
200
|
+
command_name: Command name for error messages.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Tuple of (api_url, api_key).
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
click.Abort: If existing URL is missing.
|
|
207
|
+
"""
|
|
208
|
+
if not existing_url:
|
|
209
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' is missing an API URL. Provide --url to set it.[/]")
|
|
210
|
+
raise click.Abort()
|
|
211
|
+
return _get_credentials_non_interactive(existing_url, True, name, command_name)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _preserve_existing_values(
|
|
215
|
+
api_url: str,
|
|
216
|
+
api_key: str,
|
|
217
|
+
existing_url: str,
|
|
218
|
+
existing_key: str,
|
|
219
|
+
) -> tuple[str, str]:
|
|
220
|
+
"""Preserve stored values when blank input is provided during edit.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
api_url: Collected API URL.
|
|
224
|
+
api_key: Collected API key.
|
|
225
|
+
existing_url: Existing account URL.
|
|
226
|
+
existing_key: Existing account key.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tuple of (api_url, api_key) with preserved values.
|
|
230
|
+
"""
|
|
231
|
+
if not api_url and existing_url:
|
|
232
|
+
api_url = existing_url
|
|
233
|
+
if not api_key and existing_key:
|
|
234
|
+
api_key = existing_key
|
|
235
|
+
return api_url, api_key
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _collect_credentials_from_inputs(
|
|
239
|
+
url: str | None,
|
|
240
|
+
read_key_from_stdin: bool,
|
|
241
|
+
name: str,
|
|
242
|
+
existing: dict[str, str] | None,
|
|
243
|
+
command_name: str,
|
|
244
|
+
existing_url: str,
|
|
245
|
+
) -> tuple[str, str]:
|
|
246
|
+
"""Collect credentials based on input flags and existing data.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
url: Optional URL from flag.
|
|
250
|
+
read_key_from_stdin: Whether to read key from stdin.
|
|
251
|
+
name: Account name (for error messages).
|
|
252
|
+
existing: Existing account data.
|
|
253
|
+
command_name: Command name for error messages.
|
|
254
|
+
existing_url: Existing account URL.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Tuple of (api_url, api_key).
|
|
258
|
+
"""
|
|
259
|
+
if url and read_key_from_stdin:
|
|
260
|
+
# Non-interactive: URL from flag, key from stdin
|
|
261
|
+
return _get_credentials_non_interactive(url, True, name, command_name)
|
|
262
|
+
if url:
|
|
263
|
+
# URL provided, prompt for key
|
|
264
|
+
return _get_credentials_non_interactive(url, False, name, command_name)
|
|
265
|
+
if read_key_from_stdin and existing:
|
|
266
|
+
# Key rotation using stored URL
|
|
267
|
+
return _handle_key_rotation(name, existing_url, command_name)
|
|
268
|
+
# Fully interactive or error case
|
|
269
|
+
return _get_credentials_interactive(read_key_from_stdin, existing)
|
|
270
|
+
|
|
271
|
+
|
|
181
272
|
def _collect_account_credentials(
|
|
182
273
|
url: str | None,
|
|
183
274
|
read_key_from_stdin: bool,
|
|
@@ -198,15 +289,16 @@ def _collect_account_credentials(
|
|
|
198
289
|
Raises:
|
|
199
290
|
click.Abort: If credentials cannot be collected or are invalid.
|
|
200
291
|
"""
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
292
|
+
command_name = "aip accounts edit" if existing else "aip accounts add"
|
|
293
|
+
existing_url = existing.get("api_url", "") if existing else ""
|
|
294
|
+
existing_key = existing.get("api_key", "") if existing else ""
|
|
295
|
+
|
|
296
|
+
api_url, api_key = _collect_credentials_from_inputs(
|
|
297
|
+
url, read_key_from_stdin, name, existing, command_name, existing_url
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Preserve stored values when blank input is provided during edit
|
|
301
|
+
api_url, api_key = _preserve_existing_values(api_url, api_key, existing_url, existing_key)
|
|
210
302
|
|
|
211
303
|
if not api_url or not api_key:
|
|
212
304
|
console.print(f"[{ERROR_STYLE}]Error: Both API URL and API key are required.[/]")
|
|
@@ -235,12 +327,15 @@ def add_account(
|
|
|
235
327
|
read_key_from_stdin: bool,
|
|
236
328
|
overwrite: bool,
|
|
237
329
|
) -> None:
|
|
238
|
-
"""Add
|
|
330
|
+
"""Add a new account profile.
|
|
239
331
|
|
|
240
332
|
NAME is the account name (1-32 chars, alphanumeric, dash, underscore).
|
|
241
333
|
|
|
242
334
|
By default, this command runs interactively, prompting for API URL and key.
|
|
243
335
|
For non-interactive use, both --url and --key (stdin) are required.
|
|
336
|
+
|
|
337
|
+
If the account already exists, use --yes to overwrite without prompting.
|
|
338
|
+
To update an existing account, use [bold]aip accounts edit <name>[/bold] instead.
|
|
244
339
|
"""
|
|
245
340
|
store = get_account_store()
|
|
246
341
|
|
|
@@ -263,6 +358,55 @@ def add_account(
|
|
|
263
358
|
raise click.Abort() from e
|
|
264
359
|
|
|
265
360
|
|
|
361
|
+
@accounts_group.command("edit")
|
|
362
|
+
@click.argument("name")
|
|
363
|
+
@click.option("--url", help="API URL (optional, leave blank to keep current)")
|
|
364
|
+
@click.option(
|
|
365
|
+
"--key",
|
|
366
|
+
"read_key_from_stdin",
|
|
367
|
+
is_flag=True,
|
|
368
|
+
help="Read API key from stdin (secure, for scripts). Uses stored URL unless --url is provided.",
|
|
369
|
+
)
|
|
370
|
+
def edit_account(
|
|
371
|
+
name: str,
|
|
372
|
+
url: str | None,
|
|
373
|
+
read_key_from_stdin: bool,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Edit an existing account profile's URL or key.
|
|
376
|
+
|
|
377
|
+
NAME is the account name to edit.
|
|
378
|
+
|
|
379
|
+
By default, this command runs interactively, showing current values and
|
|
380
|
+
prompting for new ones. Leave fields blank to keep current values.
|
|
381
|
+
|
|
382
|
+
For non-interactive use, provide --url to change the URL, --key (stdin) to rotate the key,
|
|
383
|
+
or both. Stored values are reused for any fields not provided.
|
|
384
|
+
"""
|
|
385
|
+
store = get_account_store()
|
|
386
|
+
|
|
387
|
+
# Account must exist for edit
|
|
388
|
+
existing = store.get_account(name)
|
|
389
|
+
if not existing:
|
|
390
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
|
|
391
|
+
console.print(f"Use [bold]aip accounts add {name}[/bold] to create a new account.")
|
|
392
|
+
raise click.Abort()
|
|
393
|
+
|
|
394
|
+
# Collect credentials (will pre-fill existing values in interactive mode)
|
|
395
|
+
api_url, api_key = _collect_account_credentials(url, read_key_from_stdin, name, existing)
|
|
396
|
+
|
|
397
|
+
# Save account
|
|
398
|
+
try:
|
|
399
|
+
store.add_account(name, api_url, api_key, overwrite=True)
|
|
400
|
+
console.print(Text(f"✅ Account '{name}' updated successfully", style=SUCCESS_STYLE))
|
|
401
|
+
_print_active_account_footer(store)
|
|
402
|
+
except InvalidAccountNameError as e:
|
|
403
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
404
|
+
raise click.Abort() from e
|
|
405
|
+
except AccountStoreError as e:
|
|
406
|
+
console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
|
|
407
|
+
raise click.Abort() from e
|
|
408
|
+
|
|
409
|
+
|
|
266
410
|
@accounts_group.command("use")
|
|
267
411
|
@click.argument("name")
|
|
268
412
|
def use_account(name: str) -> None:
|
|
@@ -281,7 +425,8 @@ def use_account(name: str) -> None:
|
|
|
281
425
|
|
|
282
426
|
if not url or not api_key:
|
|
283
427
|
console.print(
|
|
284
|
-
f"[{ERROR_STYLE}]Error: Account '{name}' is missing credentials.
|
|
428
|
+
f"[{ERROR_STYLE}]Error: Account '{name}' is missing credentials. "
|
|
429
|
+
f"Use [bold]aip accounts edit {name}[/bold] to update credentials.[/]"
|
|
285
430
|
)
|
|
286
431
|
raise click.Abort()
|
|
287
432
|
|
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -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,12 @@ def list_agents(
|
|
|
591
594
|
f"{ICON_AGENT} Available Agents",
|
|
592
595
|
columns,
|
|
593
596
|
transform_agent,
|
|
594
|
-
skip_picker=
|
|
597
|
+
skip_picker=(
|
|
598
|
+
not interactive_enabled
|
|
599
|
+
or picker_attempted
|
|
600
|
+
or simple
|
|
601
|
+
or any(param is not None for param in (agent_type, framework, name, version))
|
|
602
|
+
),
|
|
595
603
|
use_pager=False,
|
|
596
604
|
)
|
|
597
605
|
|
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
|
|
800
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
best_match = label
|
|
830
|
+
if _fuzzy_score(query_lower, label.lower()) >= 0:
|
|
831
|
+
has_match = True
|
|
832
|
+
break
|
|
838
833
|
|
|
839
|
-
|
|
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
|
|
869
|
-
"""
|
|
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: Any) -> bool:
|
|
879
|
+
"""Case-insensitive fuzzy match with optional spaces; returns False for non-string inputs."""
|
|
880
|
+
# Ensure search is a string
|
|
881
|
+
if not isinstance(search, str) or not isinstance(target, 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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
"""
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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,61 @@ 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
|
+
suffix_cache = {label: _extract_id_suffix(label) for label in labels}
|
|
1004
|
+
|
|
1005
|
+
if not query:
|
|
1006
|
+
# No query: sort by case-insensitive label, then id suffix
|
|
1007
|
+
return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
|
|
1008
|
+
|
|
1009
|
+
# Ensure query is a string
|
|
1010
|
+
if not isinstance(query, str):
|
|
1011
|
+
return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
|
|
1012
|
+
|
|
1013
|
+
query_lower = query.lower()
|
|
1014
|
+
|
|
1015
|
+
# Calculate scores and create tuples for sorting
|
|
1016
|
+
scored_labels = []
|
|
1017
|
+
for label in labels:
|
|
1018
|
+
label_lower = label.lower()
|
|
1019
|
+
score = _fuzzy_score(query_lower, label_lower)
|
|
1020
|
+
if score >= 0: # Only include matches
|
|
1021
|
+
scored_labels.append((score, label_lower, suffix_cache[label], label))
|
|
1022
|
+
|
|
1023
|
+
if not scored_labels:
|
|
1024
|
+
# No fuzzy matches: fall back to deterministic label sorting
|
|
1025
|
+
return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
|
|
1026
|
+
|
|
1027
|
+
# Sort by: score (desc), label (case-insensitive), id suffix, original label
|
|
1028
|
+
scored_labels.sort(key=lambda x: (-x[0], x[1], x[2], x[3]))
|
|
1029
|
+
|
|
1030
|
+
return [label for _score, _label_lower, _id_suffix, label in scored_labels]
|
|
1031
|
+
|
|
1032
|
+
|
|
930
1033
|
# ----------------------- Structured renderer helpers -------------------- #
|
|
931
1034
|
|
|
932
1035
|
|
|
@@ -1172,7 +1275,12 @@ def _print_selection_tip(title: str) -> None:
|
|
|
1172
1275
|
|
|
1173
1276
|
|
|
1174
1277
|
def _handle_fuzzy_pick_selection(rows: list[dict[str, Any]], columns: list[tuple], title: str) -> bool:
|
|
1175
|
-
"""Handle fuzzy picker selection
|
|
1278
|
+
"""Handle fuzzy picker selection.
|
|
1279
|
+
|
|
1280
|
+
Returns:
|
|
1281
|
+
True if a resource was selected and displayed,
|
|
1282
|
+
False if cancelled/no selection.
|
|
1283
|
+
"""
|
|
1176
1284
|
picked = _try_fuzzy_pick(rows, columns, title)
|
|
1177
1285
|
if picked is None:
|
|
1178
1286
|
return False
|
|
@@ -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=
|
|
7
|
+
glaip_sdk/cli/auth.py,sha256=9hfjZyd4cx2_mImqykJ1sWQsuVTR2gy6D4hFqAQNKL4,24129
|
|
8
8
|
glaip_sdk/cli/commands/__init__.py,sha256=6Z3ASXDut0lAbUX_umBFtxPzzFyqoiZfVeTahThFu1A,219
|
|
9
|
-
glaip_sdk/cli/commands/accounts.py,sha256=
|
|
10
|
-
glaip_sdk/cli/commands/agents.py,sha256=
|
|
9
|
+
glaip_sdk/cli/commands/accounts.py,sha256=B5itsUzqoH_hBRYOVd2m4nPoIuBbPDIoK974zKMm9NE,18635
|
|
10
|
+
glaip_sdk/cli/commands/agents.py,sha256=35Ra1PLZYiSainYTtMBB40Iio5gDY_tyaDpeujoVdHw,47963
|
|
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=
|
|
47
|
+
glaip_sdk/cli/utils.py,sha256=fV6PZlQ7K5zckpFWvwh3yLmETGrVylK9AXtN7zKBp-A,57374
|
|
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.
|
|
111
|
-
glaip_sdk-0.5.
|
|
112
|
-
glaip_sdk-0.5.
|
|
113
|
-
glaip_sdk-0.5.
|
|
110
|
+
glaip_sdk-0.5.2.dist-info/METADATA,sha256=yYVEtAsyIJBd3p6bgZxlvSIzUeOwSK-tI3DQVPAP0tI,7053
|
|
111
|
+
glaip_sdk-0.5.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
112
|
+
glaip_sdk-0.5.2.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
|
|
113
|
+
glaip_sdk-0.5.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|