glaip-sdk 0.5.1__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.
@@ -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(url: str, read_key_from_stdin: bool, name: str) -> tuple[str, str]:
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 | aip accounts add {name} --url {url} --key[/]",
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 url and read_key_from_stdin:
202
- # Non-interactive: URL from flag, key from stdin
203
- api_url, api_key = _get_credentials_non_interactive(url, True, name)
204
- elif url:
205
- # URL provided, prompt for key
206
- api_url, api_key = _get_credentials_non_interactive(url, False, name)
207
- else:
208
- # Fully interactive or error case
209
- api_url, api_key = _get_credentials_interactive(read_key_from_stdin, existing)
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 or update an account profile.
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. Re-run 'aip accounts add {name}'.[/]"
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
 
@@ -594,9 +594,12 @@ def list_agents(
594
594
  f"{ICON_AGENT} Available Agents",
595
595
  columns,
596
596
  transform_agent,
597
- skip_picker=picker_attempted
598
- or simple
599
- or any(param is not None for param in (agent_type, framework, name, version)),
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
+ ),
600
603
  use_pager=False,
601
604
  )
602
605
 
glaip_sdk/cli/utils.py CHANGED
@@ -875,10 +875,10 @@ def _strip_spaces_for_matching(value: str) -> str:
875
875
  return re.sub(r"\s+", "", value)
876
876
 
877
877
 
878
- def _is_fuzzy_match(search: Any, target: str) -> bool:
879
- """Case-insensitive fuzzy match with optional spaces."""
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
880
  # Ensure search is a string
881
- if not isinstance(search, str):
881
+ if not isinstance(search, str) or not isinstance(target, str):
882
882
  return False
883
883
 
884
884
  if not search:
@@ -1000,13 +1000,15 @@ def _rank_labels(labels: list[str], query: Any) -> list[str]:
1000
1000
  Labels sorted by fuzzy score (descending), then case-insensitive label,
1001
1001
  then id suffix for deterministic ordering.
1002
1002
  """
1003
+ suffix_cache = {label: _extract_id_suffix(label) for label in labels}
1004
+
1003
1005
  if not query:
1004
1006
  # No query: sort by case-insensitive label, then id suffix
1005
- return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1007
+ return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
1006
1008
 
1007
1009
  # Ensure query is a string
1008
1010
  if not isinstance(query, str):
1009
- return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1011
+ return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
1010
1012
 
1011
1013
  query_lower = query.lower()
1012
1014
 
@@ -1016,11 +1018,11 @@ def _rank_labels(labels: list[str], query: Any) -> list[str]:
1016
1018
  label_lower = label.lower()
1017
1019
  score = _fuzzy_score(query_lower, label_lower)
1018
1020
  if score >= 0: # Only include matches
1019
- scored_labels.append((score, label_lower, _extract_id_suffix(label), label))
1021
+ scored_labels.append((score, label_lower, suffix_cache[label], label))
1020
1022
 
1021
1023
  if not scored_labels:
1022
1024
  # No fuzzy matches: fall back to deterministic label sorting
1023
- return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1025
+ return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
1024
1026
 
1025
1027
  # Sort by: score (desc), label (case-insensitive), id suffix, original label
1026
1028
  scored_labels.sort(key=lambda x: (-x[0], x[1], x[2], x[3]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: glaip-sdk
3
- Version: 0.5.1
3
+ Version: 0.5.2
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
@@ -6,8 +6,8 @@ glaip_sdk/cli/account_store.py,sha256=NXuAVPaJS_32Aw1VTaZCNwIID-gADw4F_UMieoWmg3
6
6
  glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
7
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=VCG-JZGY86DlWO5bAfDZ70RuyKQ5q-Rh4U0iM8NwO6M,13755
10
- glaip_sdk/cli/commands/agents.py,sha256=y89okY-a5sM_QCS3F3C66DF7yhhHFbUJ7ZzIl2DUEck,47880
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=iPtt4xAqtCW-dwQ-JWVwoPVPAm-P1R8C-1kih6ZIYXU,57255
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.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,,
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,,