glaip-sdk 0.4.0__py3-none-any.whl → 0.5.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.
@@ -0,0 +1,65 @@
1
+ """Shared helpers for configuration/account flows."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.text import Text
6
+
7
+ from glaip_sdk import Client
8
+ from glaip_sdk.branding import PRIMARY, SUCCESS_STYLE, WARNING_STYLE, AIPBranding
9
+ from glaip_sdk.cli.utils import sdk_version
10
+
11
+
12
+ def render_branding_header(console: Console, rule_text: str) -> None:
13
+ """Render the standard CLI branding header with a custom rule text."""
14
+ branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
15
+ heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
16
+ console.print(heading)
17
+ console.print()
18
+ console.print(branding.get_welcome_banner())
19
+ console.rule(rule_text, style=PRIMARY)
20
+
21
+
22
+ def check_connection(
23
+ api_url: str,
24
+ api_key: str,
25
+ console: Console,
26
+ *,
27
+ abort_on_error: bool = False,
28
+ extra_hint: str | None = None,
29
+ ) -> bool:
30
+ """Test connectivity and report results.
31
+
32
+ Returns True on success, False on handled failures. Raises click.Abort when
33
+ abort_on_error is True and a fatal error occurs.
34
+ """
35
+ console.print("\n🔌 Testing connection...")
36
+ client: Client | None = None
37
+ try:
38
+ # Import lazily so test patches targeting glaip_sdk.Client are honored
39
+ from importlib import import_module # noqa: PLC0415
40
+
41
+ client_module = import_module("glaip_sdk")
42
+ client = client_module.Client(api_url=api_url, api_key=api_key)
43
+ try:
44
+ agents = client.list_agents()
45
+ console.print(Text(f"✅ Connection successful! Found {len(agents)} agents", style=SUCCESS_STYLE))
46
+ return True
47
+ except Exception as exc: # pragma: no cover - API failures depend on network
48
+ console.print(Text(f"⚠️ Connection established but API call failed: {exc}", style=WARNING_STYLE))
49
+ console.print(" You may need to check your API permissions or network access")
50
+ if extra_hint:
51
+ console.print(extra_hint)
52
+ if abort_on_error:
53
+ raise click.Abort() from exc
54
+ return False
55
+ except Exception as exc:
56
+ console.print(Text(f"❌ Connection failed: {exc}"))
57
+ console.print(" Please check your API URL and key")
58
+ if extra_hint:
59
+ console.print(extra_hint)
60
+ if abort_on_error:
61
+ raise click.Abort() from exc
62
+ return False
63
+ finally:
64
+ if client is not None:
65
+ client.close()
@@ -10,43 +10,53 @@ import click
10
10
  from rich.console import Console
11
11
  from rich.text import Text
12
12
 
13
- from glaip_sdk import Client
14
- from glaip_sdk.branding import (
15
- ACCENT_STYLE,
16
- ERROR_STYLE,
17
- INFO,
18
- PRIMARY,
19
- SUCCESS,
20
- SUCCESS_STYLE,
21
- WARNING_STYLE,
22
- AIPBranding,
23
- )
13
+ from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
14
+ from glaip_sdk.cli.account_store import get_account_store
15
+ from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
24
16
  from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
25
- from glaip_sdk.cli.rich_helpers import markup_text
26
17
  from glaip_sdk.cli.hints import format_command_hint
27
- from glaip_sdk.cli.utils import command_hint, sdk_version
18
+ from glaip_sdk.cli.masking import mask_api_key_display
19
+ from glaip_sdk.cli.rich_helpers import markup_text
20
+ from glaip_sdk.cli.utils import command_hint
28
21
  from glaip_sdk.icons import ICON_TOOL
29
22
  from glaip_sdk.rich_components import AIPTable
30
23
 
31
24
  console = Console()
32
25
 
26
+ # Shared deprecation banner for legacy config commands
27
+ CONFIG_DEPRECATION_MSG = (
28
+ f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in a future release. "
29
+ "Use 'aip accounts ...' (list/add/use/remove) or 'aip configure' for the wizard.[/]"
30
+ )
31
+
32
+
33
+ def _print_config_deprecation() -> None:
34
+ """Print a standardized deprecation warning for legacy config commands."""
35
+ console.print(CONFIG_DEPRECATION_MSG)
36
+
33
37
 
34
38
  @click.group()
35
39
  def config_group() -> None:
36
40
  """Configuration management operations."""
37
- pass
41
+ _print_config_deprecation()
38
42
 
39
43
 
40
44
  @config_group.command("list")
41
- def list_config() -> None:
42
- """List current configuration."""
43
- config = load_config()
45
+ @click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
46
+ @click.pass_context
47
+ def list_config(ctx: click.Context, output_json: bool) -> None:
48
+ """List current configuration.
44
49
 
45
- if not config:
46
- _print_missing_config_hint()
47
- return
50
+ Deprecated: run 'aip accounts list' for profile-aware output.
51
+ """
52
+ console.print(f"[{WARNING_STYLE}]Deprecated: run 'aip accounts list' for profile-aware output.[/]")
53
+
54
+ # Delegate to accounts list by invoking the command
55
+ from glaip_sdk.cli.commands.accounts import accounts_group # noqa: PLC0415
48
56
 
49
- _render_config_table(config)
57
+ list_cmd = accounts_group.get_command(ctx, "list")
58
+ if list_cmd:
59
+ ctx.invoke(list_cmd, output_json=output_json)
50
60
 
51
61
 
52
62
  CONFIG_VALUE_TYPES: dict[str, str] = {
@@ -104,14 +114,49 @@ def _coerce_config_value(key: str, raw_value: str) -> str | bool | int | float:
104
114
  @config_group.command("set")
105
115
  @click.argument("key")
106
116
  @click.argument("value")
107
- def set_config(key: str, value: str) -> None:
108
- """Set a configuration value."""
109
- valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
117
+ @click.option(
118
+ "--account",
119
+ "account_name",
120
+ help="Account name to set value for (defaults to active account)",
121
+ )
122
+ def set_config(key: str, value: str, account_name: str | None) -> None:
123
+ """Set a configuration value.
110
124
 
125
+ For api_url and api_key, this operates on the specified account (or active account).
126
+ Other keys (timeout, history_default_limit) are global settings.
127
+ """
128
+ # For other keys, use legacy config
129
+ valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
111
130
  if key not in valid_keys:
112
131
  console.print(f"[{ERROR_STYLE}]Error: Invalid key '{key}'. Valid keys are: {', '.join(valid_keys)}[/]")
113
132
  raise click.ClickException(f"Invalid configuration key: {key}")
114
133
 
134
+ store = get_account_store()
135
+ # For api_url and api_key, update account profile but also mirror to legacy config
136
+ if key in ("api_url", "api_key"):
137
+ target_account = account_name or store.get_active_account() or "default"
138
+ try:
139
+ account = store.get_account(target_account) or {}
140
+ account[key] = value
141
+ store.add_account(
142
+ target_account,
143
+ account.get("api_url", ""),
144
+ account.get("api_key", ""),
145
+ overwrite=True,
146
+ )
147
+ except Exception:
148
+ # If account store persistence fails (e.g., mocked I/O), continue with legacy config
149
+ pass
150
+
151
+ # Always update legacy config for backward compatibility and test isolation
152
+ legacy_config = load_config()
153
+ legacy_config[key] = value
154
+ save_config(legacy_config)
155
+
156
+ display_value = _mask_api_key(value) if key == "api_key" else value
157
+ console.print(Text(f"✅ Set {key} = {display_value} for account '{target_account}'", style=SUCCESS_STYLE))
158
+ return
159
+
115
160
  coerced_value = _coerce_config_value(key, value)
116
161
  config = load_config()
117
162
  config[key] = coerced_value
@@ -127,12 +172,19 @@ def get_config(key: str) -> None:
127
172
  """Get a configuration value."""
128
173
  config = load_config()
129
174
 
130
- if key not in config:
175
+ value = config.get(key)
176
+
177
+ # Fallback to account store for api_url/api_key when legacy config lacks the key
178
+ if value is None and key in {"api_url", "api_key"}:
179
+ store = get_account_store()
180
+ active = store.get_active_account() or "default"
181
+ account = store.get_account(active) or {}
182
+ value = account.get(key)
183
+
184
+ if value is None:
131
185
  console.print(markup_text(f"[{WARNING_STYLE}]Configuration key '{key}' not found.[/]"))
132
186
  raise click.ClickException(f"Configuration key not found: {key}")
133
187
 
134
- value = config[key]
135
-
136
188
  if key == "api_key":
137
189
  console.print(_mask_api_key(value))
138
190
  else:
@@ -190,41 +242,73 @@ def reset_config(force: bool) -> None:
190
242
  console.print(message)
191
243
 
192
244
 
193
- def _configure_interactive() -> None:
245
+ def _configure_interactive(account_name: str | None = None) -> None:
194
246
  """Shared configuration logic for both configure commands."""
247
+ store = get_account_store()
248
+
249
+ # Determine account name (use provided, active, or default)
250
+ if not account_name:
251
+ account_name = store.get_active_account() or "default"
252
+
253
+ # Get existing account if it exists
254
+ existing = store.get_account(account_name)
255
+
195
256
  _render_configuration_header()
196
- config = load_config()
197
- _prompt_configuration_inputs(config)
198
- _save_configuration(config)
199
- _test_and_report_connection(config)
257
+ config = _prompt_configuration_inputs_for_account(existing)
258
+
259
+ # Save to account store
260
+ api_url = config.get("api_url", "")
261
+ api_key = config.get("api_key", "")
262
+ if api_url and api_key:
263
+ store.add_account(account_name, api_url, api_key, overwrite=True)
264
+ console.print(Text(f"\n✅ Configuration saved to account '{account_name}'", style=SUCCESS_STYLE))
265
+
266
+ _test_and_report_connection_for_account(account_name)
200
267
  _print_post_configuration_hints()
268
+ # Show active account footer
269
+ from glaip_sdk.cli.commands.accounts import _print_active_account_footer # noqa: PLC0415
270
+
271
+ _print_active_account_footer(store)
201
272
 
202
273
 
203
274
  @config_group.command()
204
- def configure() -> None:
205
- """Configure AIP CLI credentials and settings interactively."""
206
- _configure_interactive()
275
+ @click.option(
276
+ "--account",
277
+ "account_name",
278
+ help="Account name to configure (defaults to active account)",
279
+ )
280
+ def configure(account_name: str | None) -> None:
281
+ """Configure AIP CLI credentials and settings interactively.
282
+
283
+ This command is an alias for 'aip accounts add <name>' and will
284
+ configure the specified account (or active account if not specified).
285
+ """
286
+ _configure_interactive(account_name)
207
287
 
208
288
 
209
289
  # Alias command for backward compatibility
210
290
  @click.command()
211
- def configure_command() -> None:
291
+ @click.option(
292
+ "--account",
293
+ "account_name",
294
+ help="Account name to configure (defaults to active account)",
295
+ )
296
+ def configure_command(account_name: str | None) -> None:
212
297
  """Configure AIP CLI credentials and settings interactively.
213
298
 
214
299
  This is an alias for 'aip config configure' for backward compatibility.
300
+ For multi-account support, use 'aip accounts add <name>' instead.
215
301
  """
302
+ console.print(
303
+ f"[{WARNING_STYLE}]Setup tip:[/] Prefer 'aip accounts add <name>' or 'aip configure' from your terminal for "
304
+ "multi-account setup. Launching the interactive wizard now..."
305
+ )
216
306
  # Delegate to the shared function
217
- _configure_interactive()
307
+ _configure_interactive(account_name)
218
308
 
219
309
 
220
310
  # Note: The config command group should be registered in main.py
221
-
222
-
223
- def _mask_api_key(value: str | None) -> str:
224
- """Return a redacted API key string suitable for display."""
225
- if not value:
226
- return ""
227
- return "***" + value[-4:] if len(value) > 4 else "***"
311
+ _mask_api_key = mask_api_key_display
228
312
 
229
313
 
230
314
  def _print_missing_config_hint() -> None:
@@ -251,23 +335,23 @@ def _render_config_table(config: dict[str, str]) -> None:
251
335
 
252
336
  def _render_configuration_header() -> None:
253
337
  """Display the interactive configuration heading/banner."""
254
- branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
255
- heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
256
- console.print(heading)
257
- console.print()
258
- console.print(branding.get_welcome_banner())
259
- console.rule("[bold]AIP Configuration[/bold]", style=PRIMARY)
338
+ render_branding_header(console, "[bold]AIP Configuration[/bold]")
260
339
 
261
340
 
262
- def _prompt_configuration_inputs(config: dict[str, str]) -> None:
263
- """Interactively prompt for configuration values."""
341
+ def _prompt_configuration_inputs_for_account(existing: dict[str, str] | None) -> dict[str, str]:
342
+ """Interactively prompt for account configuration values."""
264
343
  console.print("\n[bold]Enter your AIP configuration:[/bold]")
265
- console.print("(Leave blank to keep current values)")
344
+ if existing:
345
+ console.print("(Leave blank to keep current values)")
266
346
  console.print("─" * 50)
267
347
 
348
+ config = existing.copy() if existing else {}
349
+
268
350
  _prompt_api_url(config)
269
351
  _prompt_api_key(config)
270
352
 
353
+ return config
354
+
271
355
 
272
356
  def _prompt_api_url(config: dict[str, str]) -> None:
273
357
  """Ask the user for the API URL, preserving existing values by default."""
@@ -291,43 +375,24 @@ def _prompt_api_key(config: dict[str, str]) -> None:
291
375
  config["api_key"] = new_key
292
376
 
293
377
 
294
- def _save_configuration(config: dict[str, str]) -> None:
295
- """Persist the collected configuration to disk."""
296
- save_config(config)
297
- console.print(Text(f"\n✅ Configuration saved to: {CONFIG_FILE}", style=SUCCESS_STYLE))
378
+ def _test_and_report_connection_for_account(account_name: str) -> None:
379
+ """Sanity-check the provided credentials against the backend."""
380
+ store = get_account_store()
381
+ account = store.get_account(account_name)
382
+ if not account:
383
+ return
298
384
 
385
+ api_url = account.get("api_url", "")
386
+ api_key = account.get("api_key", "")
387
+ if not api_url or not api_key:
388
+ return
299
389
 
300
- def _test_and_report_connection(config: dict[str, str]) -> None:
301
- """Sanity-check the provided credentials against the backend."""
302
- console.print("\n🔌 Testing connection...")
303
- client: Client | None = None
304
- try:
305
- client = Client(api_url=config["api_url"], api_key=config["api_key"])
306
- try:
307
- agents = client.list_agents()
308
- console.print(
309
- Text(
310
- f"✅ Connection successful! Found {len(agents)} agents",
311
- style=SUCCESS_STYLE,
312
- )
313
- )
314
- except Exception as exc: # pragma: no cover - API failures depend on network
315
- console.print(
316
- Text(
317
- f"⚠️ Connection established but API call failed: {exc}",
318
- style=WARNING_STYLE,
319
- )
320
- )
321
- console.print(" You may need to check your API permissions or network access")
322
- except Exception as exc:
323
- console.print(Text(f"❌ Connection failed: {exc}"))
324
- console.print(" Please check your API URL and key")
325
- hint_status = command_hint("status", slash_command="status")
326
- if hint_status:
327
- console.print(f" You can run {format_command_hint(hint_status) or hint_status} later to test again")
328
- finally:
329
- if client is not None:
330
- client.close()
390
+ hint_status = command_hint("status", slash_command="status")
391
+ extra_hint = None
392
+ if hint_status:
393
+ extra_hint = f" You can run {format_command_hint(hint_status) or hint_status} later to test again"
394
+
395
+ check_connection(api_url, api_key, console, abort_on_error=False, extra_hint=extra_hint)
331
396
 
332
397
 
333
398
  def _print_post_configuration_hints() -> None:
@@ -36,8 +36,8 @@ from glaip_sdk.cli.transcript.history import (
36
36
  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
- from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
40
39
  from glaip_sdk.utils.rendering.layout.panels import create_final_panel
40
+ from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
41
41
 
42
42
  console = Console()
43
43
 
glaip_sdk/cli/config.py CHANGED
@@ -5,12 +5,25 @@ Authors:
5
5
  """
6
6
 
7
7
  import os
8
+ from copy import deepcopy
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
11
12
  import yaml
12
13
 
13
- CONFIG_DIR = Path.home() / ".aip"
14
+ _ENV_CONFIG_DIR = os.getenv("AIP_CONFIG_DIR")
15
+ _TEST_ENV = os.getenv("PYTEST_CURRENT_TEST") or os.getenv("PYTEST_XDIST_WORKER")
16
+
17
+ if _ENV_CONFIG_DIR:
18
+ CONFIG_DIR = Path(_ENV_CONFIG_DIR)
19
+ elif _TEST_ENV:
20
+ # Isolate test runs (including xdist workers) from the real user config directory
21
+ import tempfile
22
+
23
+ CONFIG_DIR = Path(tempfile.gettempdir()) / "aip-test-config"
24
+ else: # pragma: no cover - default path used outside test runs
25
+ CONFIG_DIR = Path.home() / ".aip"
26
+
14
27
  CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
28
  _ALLOWED_KEYS = {
16
29
  "api_url",
@@ -18,13 +31,28 @@ _ALLOWED_KEYS = {
18
31
  "timeout",
19
32
  "history_default_limit",
20
33
  }
34
+ # Keys that must be preserved for multi-account support
35
+ _PRESERVE_KEYS = {
36
+ "version",
37
+ "active_account",
38
+ "accounts",
39
+ }
21
40
 
22
41
 
23
42
  def _sanitize_config(data: dict[str, Any] | None) -> dict[str, Any]:
24
- """Return config filtered to allowed keys only."""
43
+ """Return config filtered to allowed keys only, preserving multi-account keys."""
25
44
  if not data:
26
45
  return {}
27
- return {k: v for k, v in data.items() if k in _ALLOWED_KEYS}
46
+ result: dict[str, Any] = {}
47
+ # Preserve multi-account structure (defensively copy to avoid callers mutating source)
48
+ for key in _PRESERVE_KEYS:
49
+ if key in data:
50
+ result[key] = deepcopy(data[key])
51
+ # Add allowed legacy keys (copied to avoid side effects)
52
+ for key in _ALLOWED_KEYS:
53
+ if key in data:
54
+ result[key] = deepcopy(data[key])
55
+ return result
28
56
 
29
57
 
30
58
  def load_config() -> dict[str, Any]:
glaip_sdk/cli/display.py CHANGED
@@ -16,8 +16,8 @@ from rich.panel import Panel
16
16
  from rich.text import Text
17
17
 
18
18
  from glaip_sdk.branding import ERROR_STYLE, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
19
- from glaip_sdk.cli.rich_helpers import markup_text
20
19
  from glaip_sdk.cli.hints import command_hint, format_command_hint, in_slash_mode
20
+ from glaip_sdk.cli.rich_helpers import markup_text
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 CHANGED
@@ -6,7 +6,6 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
-
10
9
  import click
11
10
 
12
11
  from glaip_sdk.branding import HINT_COMMAND_STYLE, HINT_DESCRIPTION_COLOR