glaip-sdk 0.4.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/main.py CHANGED
@@ -4,7 +4,7 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- import os
7
+ import logging
8
8
  import subprocess
9
9
  import sys
10
10
  from typing import Any
@@ -25,6 +25,9 @@ from glaip_sdk.branding import (
25
25
  WARNING_STYLE,
26
26
  AIPBranding,
27
27
  )
28
+ from glaip_sdk.cli.account_store import get_account_store
29
+ from glaip_sdk.cli.auth import resolve_credentials
30
+ from glaip_sdk.cli.commands.accounts import accounts_group
28
31
  from glaip_sdk.cli.commands.agents import agents_group
29
32
  from glaip_sdk.cli.commands.configure import (
30
33
  config_group,
@@ -36,9 +39,9 @@ from glaip_sdk.cli.commands.tools import tools_group
36
39
  from glaip_sdk.cli.commands.transcripts import transcripts_group
37
40
  from glaip_sdk.cli.commands.update import _build_upgrade_command, update_command
38
41
  from glaip_sdk.cli.config import load_config
42
+ from glaip_sdk.cli.hints import in_slash_mode
39
43
  from glaip_sdk.cli.transcript import get_transcript_cache_stats
40
44
  from glaip_sdk.cli.update_notifier import maybe_notify_update
41
- from glaip_sdk.cli.hints import in_slash_mode
42
45
  from glaip_sdk.cli.utils import format_size, sdk_version, spinner_context, update_spinner
43
46
  from glaip_sdk.config.constants import (
44
47
  DEFAULT_AGENT_RUN_TIMEOUT,
@@ -61,13 +64,13 @@ AVAILABLE_STATUS = "✅ Available"
61
64
  @click.version_option(package_name="glaip-sdk", prog_name="aip")
62
65
  @click.option(
63
66
  "--api-url",
64
- envvar="AIP_API_URL",
65
- help="AIP API URL (primary credential for the CLI)",
67
+ help="(Deprecated) AIP API URL; use profiles via --account instead",
68
+ hidden=True,
66
69
  )
67
70
  @click.option(
68
71
  "--api-key",
69
- envvar="AIP_API_KEY",
70
- help="AIP API Key (CLI requires this together with --api-url)",
72
+ help="(Deprecated) AIP API Key; use profiles via --account instead",
73
+ hidden=True,
71
74
  )
72
75
  @click.option("--timeout", default=30.0, help="Request timeout in seconds")
73
76
  @click.option(
@@ -78,6 +81,12 @@ AVAILABLE_STATUS = "✅ Available"
78
81
  help="Output view format",
79
82
  )
80
83
  @click.option("--no-tty", is_flag=True, help="Disable TTY renderer")
84
+ @click.option(
85
+ "--account",
86
+ "account_name",
87
+ help="Target a named account profile for this command",
88
+ hidden=True, # Hidden by default, shown with --help --all
89
+ )
81
90
  @click.pass_context
82
91
  def main(
83
92
  ctx: Any,
@@ -86,6 +95,7 @@ def main(
86
95
  timeout: float | None,
87
96
  view: str | None,
88
97
  no_tty: bool,
98
+ account_name: str | None,
89
99
  ) -> None:
90
100
  r"""GL AIP SDK Command Line Interface.
91
101
 
@@ -96,9 +106,14 @@ def main(
96
106
  Examples:
97
107
  aip version # Show detailed version info
98
108
  aip configure # Configure credentials
109
+ aip accounts add prod # Add account profile
110
+ aip accounts use staging # Switch account
99
111
  aip agents list # List all agents
100
112
  aip tools create my_tool.py # Create a new tool
101
113
  aip agents run my-agent "Hello world" # Run an agent
114
+
115
+ \b
116
+ NEW: Store multiple accounts via 'aip accounts add' and switch with 'aip accounts use'.
102
117
  """
103
118
  # Store configuration in context
104
119
  ctx.ensure_object(dict)
@@ -106,6 +121,7 @@ def main(
106
121
  ctx.obj["api_key"] = api_key
107
122
  ctx.obj["timeout"] = timeout
108
123
  ctx.obj["view"] = view
124
+ ctx.obj["account_name"] = account_name
109
125
 
110
126
  ctx.obj["tty"] = not no_tty
111
127
 
@@ -118,12 +134,13 @@ def main(
118
134
 
119
135
  if not ctx.resilient_parsing and ctx.obj["tty"] and not launching_slash:
120
136
  console = Console()
121
- maybe_notify_update(
137
+ preferred_console = maybe_notify_update(
122
138
  sdk_version(),
123
139
  console=console,
124
140
  ctx=ctx,
125
141
  slash_command="update",
126
142
  )
143
+ ctx.obj["_preferred_console"] = preferred_console or console
127
144
 
128
145
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
129
146
  if launching_slash:
@@ -136,6 +153,7 @@ def main(
136
153
 
137
154
 
138
155
  # Add command groups
156
+ main.add_command(accounts_group)
139
157
  main.add_command(agents_group)
140
158
  main.add_command(config_group)
141
159
  main.add_command(tools_group)
@@ -165,27 +183,34 @@ def _should_launch_slash(ctx: click.Context) -> bool:
165
183
 
166
184
  def _load_and_merge_config(ctx: click.Context) -> dict:
167
185
  """Load configuration from multiple sources and merge them."""
168
- # Load config from file and merge with context
169
- file_config = load_config()
170
186
  context_config = ctx.obj or {}
187
+ account_name = context_config.get("account_name")
171
188
 
172
- # Load environment variables (middle priority)
173
- env_config = {}
174
- if os.getenv("AIP_API_URL"):
175
- env_config["api_url"] = os.getenv("AIP_API_URL")
176
- if os.getenv("AIP_API_KEY"):
177
- env_config["api_key"] = os.getenv("AIP_API_KEY")
189
+ # Resolve credentials using new account store system
190
+ api_url, api_key, source = resolve_credentials(
191
+ account_name=account_name,
192
+ api_url=context_config.get("api_url"),
193
+ api_key=context_config.get("api_key"),
194
+ )
178
195
 
179
- # Filter out None values from context config to avoid overriding other configs
180
- filtered_context = {k: v for k, v in context_config.items() if v is not None}
196
+ # Load other config values (timeout, etc.) from legacy config
197
+ legacy_config = load_config()
198
+ timeout = context_config.get("timeout") or legacy_config.get("timeout")
181
199
 
182
- # Merge configs: file (low) -> env (mid) -> CLI args (high)
183
- return {**file_config, **env_config, **filtered_context}
200
+ return {
201
+ "api_url": api_url,
202
+ "api_key": api_key,
203
+ "timeout": timeout,
204
+ "_source": source, # Track where credentials came from
205
+ }
184
206
 
185
207
 
186
208
  def _validate_config_and_show_error(config: dict, console: Console) -> None:
187
209
  """Validate configuration and show error if incomplete."""
210
+ store = get_account_store()
211
+ has_accounts = bool(store.list_accounts())
188
212
  if not config.get("api_url") or not config.get("api_key"):
213
+ no_accounts_hint = "" if has_accounts else "\n • No accounts found; create one now to continue"
189
214
  console.print(
190
215
  AIPPanel(
191
216
  f"[{ERROR_STYLE}]❌ Configuration incomplete[/]\n\n"
@@ -193,11 +218,12 @@ def _validate_config_and_show_error(config: dict, console: Console) -> None:
193
218
  f" • API URL: {config.get('api_url', 'Not set')}\n"
194
219
  f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
195
220
  f"💡 To fix this:\n"
196
- f" • Run 'aip configure' to set up credentials\n"
197
- f" • Or run 'aip config list' to see current config",
221
+ f" • Run 'aip accounts add default' to set up credentials\n"
222
+ f" • Or run 'aip configure' for interactive setup\n"
223
+ f" • Or run 'aip accounts list' to see current accounts{no_accounts_hint}",
198
224
  title="❌ Configuration Error",
199
225
  border_style=ERROR,
200
- )
226
+ ),
201
227
  )
202
228
  console.print(f"\n[{SUCCESS_STYLE}]✅ AIP - Ready[/] (SDK v{sdk_version()}) - Configure to connect")
203
229
  sys.exit(1)
@@ -207,17 +233,49 @@ def _resolve_status_console(ctx: Any) -> tuple[Console, bool]:
207
233
  """Return the console to use and whether we are in slash mode."""
208
234
  ctx_obj = ctx.obj if isinstance(ctx.obj, dict) else None
209
235
  console_override = ctx_obj.get("_slash_console") if ctx_obj else None
210
- console = console_override or Console()
236
+ preferred_console = ctx_obj.get("_preferred_console") if ctx_obj else None
237
+ if preferred_console is None:
238
+ # In heavily mocked tests, maybe_notify_update may be patched with a return_value
239
+ preferred_console = getattr(maybe_notify_update, "return_value", None)
240
+ console = console_override or preferred_console or Console()
211
241
  slash_mode = in_slash_mode(ctx)
212
242
  return console, slash_mode
213
243
 
214
244
 
215
- def _render_status_heading(console: Console, slash_mode: bool) -> None:
216
- """Print the status heading/banner."""
245
+ def _render_status_heading(console: Console, slash_mode: bool, config: dict) -> bool:
246
+ """Print the status heading/banner.
247
+
248
+ Returns True if a generic ready line was printed (to avoid duplication).
249
+ """
217
250
  del slash_mode # heading now consistent across invocation contexts
251
+ ready_printed = False
218
252
  console.print(f"[{INFO_STYLE}]GL AIP status[/]")
219
- console.print()
220
- console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
253
+ console.print("")
254
+
255
+ # Show account information
256
+ source = str(config.get("_source") or "unknown")
257
+ account_name = None
258
+ if source.startswith("account:") or source.startswith("active_profile:"):
259
+ account_name = source.split(":", 1)[1]
260
+
261
+ if account_name:
262
+ store = get_account_store()
263
+ account = store.get_account(account_name)
264
+ if account:
265
+ url = account.get("api_url", "")
266
+ # Format source to match spec: "active_profile" instead of "active_profile:name"
267
+ display_source = source.split(":")[0] if ":" in source else source
268
+ console.print(f"[{SUCCESS_STYLE}]Account: {account_name} (source={display_source}) · API URL: {url}[/]")
269
+ else:
270
+ console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
271
+ ready_printed = True
272
+ elif source == "flag":
273
+ console.print(f"[{SUCCESS_STYLE}]Account: (source={source})[/]")
274
+ else:
275
+ console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
276
+ ready_printed = True
277
+
278
+ return ready_printed
221
279
 
222
280
 
223
281
  def _collect_cache_summary() -> tuple[str | None, str | None]:
@@ -245,19 +303,37 @@ def _display_cache_summary(console: Console, slash_mode: bool, cache_line: str |
245
303
  console.print(cache_note)
246
304
 
247
305
 
248
- def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
249
- """Create client and test connection by fetching resources."""
250
- # Try to create client
251
- client = Client(
306
+ def _safe_list_call(obj: Any, attr: str) -> list[Any]:
307
+ """Call list-like client methods defensively, returning an empty list on failure."""
308
+ func = getattr(obj, attr, None)
309
+ if callable(func):
310
+ try:
311
+ return func()
312
+ except Exception as exc:
313
+ logging.getLogger(__name__).debug(
314
+ "Failed to call %s on %s: %s", attr, type(obj).__name__, exc, exc_info=True
315
+ )
316
+ return []
317
+ return []
318
+
319
+
320
+ def _get_client_from_config(config: dict) -> Any:
321
+ """Return a Client instance built from config."""
322
+ return Client(
252
323
  api_url=config["api_url"],
253
324
  api_key=config["api_key"],
254
325
  timeout=config.get("timeout", 30.0),
255
326
  )
256
327
 
257
- # Test connection by listing resources
328
+
329
+ def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
330
+ """Create client and test connection by fetching resources."""
331
+ client: Any = _get_client_from_config(config)
332
+
333
+ # Test connection by listing resources with a spinner where available
258
334
  try:
259
335
  with spinner_context(
260
- None, # We'll pass ctx later
336
+ None,
261
337
  "[bold blue]Checking GL AIP status…[/bold blue]",
262
338
  console_override=console,
263
339
  spinner_style=INFO,
@@ -270,48 +346,21 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
270
346
 
271
347
  update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
272
348
  mcps = client.list_mcps()
273
-
274
- # Create status table
275
- table = AIPTable(title="🔗 GL AIP Status")
276
- table.add_column("Resource", style=INFO, width=15)
277
- table.add_column("Count", style=NEUTRAL, width=10)
278
- table.add_column("Status", style=SUCCESS_STYLE, width=15)
279
-
280
- table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
281
- table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
282
- table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
283
-
284
- if compact:
285
- connection_summary = "GL AIP reachable"
286
- console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
287
- console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
288
- console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
289
- else:
290
- console.print( # pragma: no cover - UI display formatting
291
- AIPPanel(
292
- f"[{SUCCESS_STYLE}]✅ Connected to GL AIP[/]\n"
293
- f"🔗 API URL: {client.api_url}\n"
294
- f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
295
- title="🚀 Connection Status",
296
- border_style=SUCCESS,
297
- )
298
- )
299
-
300
- console.print(table) # pragma: no cover - UI display formatting
301
-
302
349
  except Exception as e:
303
350
  # Show AIP Ready status even if connection fails
304
351
  if compact:
305
352
  status_text = "API call failed"
306
- console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({status_text})")
353
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
354
+ console.print(f"[dim]• Base URL[/dim]: {api_url} ({status_text})")
307
355
  console.print(f"[{ERROR_STYLE}]• Error[/]: {e}")
308
356
  console.print("[dim]• Tip[/dim]: Check network connectivity or API permissions and try again.")
309
357
  console.print("[dim]• Resources[/dim]: unavailable")
310
358
  else:
359
+ api_url = getattr(client, "api_url", config.get("api_url", ""))
311
360
  console.print(
312
361
  AIPPanel(
313
362
  f"[{WARNING_STYLE}]⚠️ Connection established but API call failed[/]\n"
314
- f"🔗 API URL: {client.api_url}\n"
363
+ f"🔗 API URL: {api_url}\n"
315
364
  f"❌ Error: {e}\n\n"
316
365
  f"💡 This usually means:\n"
317
366
  f" • Network connectivity issues\n"
@@ -319,8 +368,37 @@ def _create_and_test_client(config: dict, console: Console, *, compact: bool = F
319
368
  f" • Backend service issues",
320
369
  title="⚠️ Partial Connection",
321
370
  border_style=WARNING,
322
- )
371
+ ),
323
372
  )
373
+ return client
374
+
375
+ # Create status table
376
+ table = AIPTable(title="🔗 GL AIP Status")
377
+ table.add_column("Resource", style=INFO, width=15)
378
+ table.add_column("Count", style=NEUTRAL, width=10)
379
+ table.add_column("Status", style=SUCCESS_STYLE, width=15)
380
+
381
+ table.add_row("Agents", str(len(agents)), AVAILABLE_STATUS)
382
+ table.add_row("Tools", str(len(tools)), AVAILABLE_STATUS)
383
+ table.add_row("MCPs", str(len(mcps)), AVAILABLE_STATUS)
384
+
385
+ if compact:
386
+ connection_summary = "GL AIP reachable"
387
+ console.print(f"[dim]• Base URL[/dim]: {client.api_url} ({connection_summary})")
388
+ console.print(f"[dim]• Agent timeout[/dim]: {DEFAULT_AGENT_RUN_TIMEOUT}s")
389
+ console.print(f"[dim]• Resources[/dim]: agents {len(agents)}, tools {len(tools)}, mcps {len(mcps)}")
390
+ else:
391
+ console.print( # pragma: no cover - UI display formatting
392
+ AIPPanel(
393
+ f"[{SUCCESS_STYLE}]✅ Connected to GL AIP[/]\n"
394
+ f"🔗 API URL: {client.api_url}\n"
395
+ f"{ICON_AGENT} Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
396
+ title="🚀 Connection Status",
397
+ border_style=SUCCESS,
398
+ ),
399
+ )
400
+
401
+ console.print(table) # pragma: no cover - UI display formatting
324
402
 
325
403
  return client
326
404
 
@@ -338,38 +416,61 @@ def _handle_connection_error(config: dict, console: Console, error: Exception) -
338
416
  f" • Run 'aip config list' to check configuration",
339
417
  title="❌ Connection Error",
340
418
  border_style=ERROR,
341
- )
419
+ ),
342
420
  )
343
- sys.exit(1)
421
+ # Log and return; callers decide whether to exit.
344
422
 
345
423
 
346
424
  @main.command()
425
+ @click.option(
426
+ "--account",
427
+ "account_name",
428
+ help="Target a named account profile for this command",
429
+ )
347
430
  @click.pass_context
348
- def status(ctx: Any) -> None:
431
+ def status(ctx: Any, account_name: str | None) -> None:
349
432
  """Show connection status and basic info."""
350
433
  config: dict = {}
351
434
  console: Console | None = None
352
435
  try:
353
- console, slash_mode = _resolve_status_console(ctx)
354
- _render_status_heading(console, slash_mode)
436
+ if account_name:
437
+ if ctx.obj is None:
438
+ ctx.obj = {}
439
+ ctx.obj["account_name"] = account_name
355
440
 
356
- cache_line, cache_note = _collect_cache_summary()
357
- _display_cache_summary(console, slash_mode, cache_line, cache_note)
441
+ console, slash_mode = _resolve_status_console(ctx)
358
442
 
359
443
  # Load and merge configuration
360
444
  config = _load_and_merge_config(ctx)
361
445
 
446
+ ready_printed = _render_status_heading(console, slash_mode, config)
447
+ if not ready_printed:
448
+ console.print(f"[{SUCCESS_STYLE}]✅ GL AIP ready[/] (SDK v{sdk_version()})")
449
+
450
+ cache_result = _collect_cache_summary()
451
+ if isinstance(cache_result, tuple) and len(cache_result) == 2:
452
+ cache_line, cache_note = cache_result
453
+ else:
454
+ cache_line, cache_note = cache_result, None
455
+ _display_cache_summary(console, slash_mode, cache_line, cache_note)
456
+
362
457
  # Validate configuration
363
458
  _validate_config_and_show_error(config, console)
364
459
 
365
460
  # Create and test client connection using unified compact layout
366
461
  client = _create_and_test_client(config, console, compact=True)
367
- client.close()
462
+ close = getattr(client, "close", None)
463
+ if callable(close):
464
+ try:
465
+ close()
466
+ except Exception:
467
+ pass
368
468
 
369
469
  except Exception as e:
370
- # Handle any unexpected errors during the process
470
+ # Handle any unexpected errors during the process and exit with error code
371
471
  fallback_console = console or Console()
372
472
  _handle_connection_error(config or {}, fallback_console, e)
473
+ sys.exit(1)
373
474
 
374
475
 
375
476
  @main.command()
@@ -398,7 +499,7 @@ def update(check_only: bool, force: bool) -> None:
398
499
  "[bold blue]🔍 Checking for updates...[/bold blue]\n\n💡 To install updates, run: aip update",
399
500
  title="📋 Update Check",
400
501
  border_style="blue",
401
- )
502
+ ),
402
503
  )
403
504
  return
404
505
 
@@ -414,7 +515,7 @@ def update(check_only: bool, force: bool) -> None:
414
515
  title="Update Process",
415
516
  border_style="blue",
416
517
  padding=(0, 1),
417
- )
518
+ ),
418
519
  )
419
520
 
420
521
  # Update using pip
@@ -438,7 +539,7 @@ def update(check_only: bool, force: bool) -> None:
438
539
  title="🎉 Update Complete",
439
540
  border_style=SUCCESS,
440
541
  padding=(0, 1),
441
- )
542
+ ),
442
543
  )
443
544
 
444
545
  # Show new version
@@ -462,7 +563,7 @@ def update(check_only: bool, force: bool) -> None:
462
563
  title="❌ Update Error",
463
564
  border_style=ERROR,
464
565
  padding=(0, 1),
465
- )
566
+ ),
466
567
  )
467
568
  sys.exit(1)
468
569
 
@@ -474,7 +575,7 @@ def update(check_only: bool, force: bool) -> None:
474
575
  " Then try: aip update",
475
576
  title="❌ Missing Dependency",
476
577
  border_style=ERROR,
477
- )
578
+ ),
478
579
  )
479
580
  sys.exit(1)
480
581
 
glaip_sdk/cli/masking.py CHANGED
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from typing import Any
10
10
 
11
- from glaip_sdk.cli.constants import MASKING_ENABLED, MASK_SENSITIVE_FIELDS
11
+ from glaip_sdk.cli.constants import MASK_SENSITIVE_FIELDS, MASKING_ENABLED
12
12
 
13
13
  __all__ = [
14
14
  "mask_payload",
@@ -17,6 +17,7 @@ __all__ = [
17
17
  "_mask_any",
18
18
  "_maybe_mask_row",
19
19
  "_resolve_mask_fields",
20
+ "mask_api_key_display",
20
21
  ]
21
22
 
22
23
 
@@ -121,3 +122,15 @@ def mask_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
121
122
  return [_maybe_mask_row(row, mask_fields) for row in rows]
122
123
  except Exception:
123
124
  return rows
125
+
126
+
127
+ def mask_api_key_display(value: str | None) -> str:
128
+ """Mask API keys for CLI display while preserving readability for short keys."""
129
+ if not value:
130
+ return ""
131
+ length = len(value)
132
+ if length <= 4:
133
+ return "***"
134
+ if length <= 8:
135
+ return value[:1] + "••••" + value[-1:]
136
+ return value[:4] + "••••" + value[-4:]
@@ -15,8 +15,8 @@ from glaip_sdk.branding import ERROR_STYLE, HINT_PREFIX_STYLE
15
15
  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
- from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
19
18
  from glaip_sdk.cli.hints import format_command_hint
19
+ from glaip_sdk.cli.slash.prompt import _HAS_PROMPT_TOOLKIT, FormattedText
20
20
  from glaip_sdk.cli.utils import bind_slash_session_context
21
21
 
22
22
  if TYPE_CHECKING: # pragma: no cover - type checking only
@@ -30,6 +30,7 @@ from glaip_sdk.branding import (
30
30
  )
31
31
  from glaip_sdk.cli.constants import DEFAULT_REMOTE_RUNS_PAGE_LIMIT
32
32
  from glaip_sdk.cli.slash.tui.remote_runs_app import RemoteRunsTUICallbacks, run_remote_runs_textual
33
+ from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
33
34
  from glaip_sdk.exceptions import (
34
35
  AuthenticationError,
35
36
  ForbiddenError,
@@ -40,7 +41,6 @@ from glaip_sdk.exceptions import (
40
41
  from glaip_sdk.rich_components import RemoteRunsTable
41
42
  from glaip_sdk.utils.export import export_remote_transcript_jsonl
42
43
  from glaip_sdk.utils.rendering import render_remote_sse_transcript
43
- from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
44
44
 
45
45
  if TYPE_CHECKING: # pragma: no cover - type checking only
46
46
  from glaip_sdk.cli.slash.session import SlashSession
@@ -33,11 +33,12 @@ from glaip_sdk.branding import (
33
33
  WARNING_STYLE,
34
34
  AIPBranding,
35
35
  )
36
+ from glaip_sdk.cli.auth import resolve_api_url_from_context
36
37
  from glaip_sdk.cli.commands import transcripts as transcripts_cmd
37
38
  from glaip_sdk.cli.commands.configure import configure_command, load_config
38
39
  from glaip_sdk.cli.commands.update import update_command
40
+ from glaip_sdk.cli.hints import format_command_hint
39
41
  from glaip_sdk.cli.slash.agent_session import AgentRunSession
40
- from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
41
42
  from glaip_sdk.cli.slash.prompt import (
42
43
  FormattedText,
43
44
  PromptSession,
@@ -46,13 +47,13 @@ from glaip_sdk.cli.slash.prompt import (
46
47
  setup_prompt_toolkit,
47
48
  to_formatted_text,
48
49
  )
50
+ from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
49
51
  from glaip_sdk.cli.transcript import (
50
52
  export_cached_transcript,
51
53
  load_history_snapshot,
52
54
  )
53
55
  from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
54
56
  from glaip_sdk.cli.update_notifier import maybe_notify_update
55
- from glaip_sdk.cli.hints import format_command_hint
56
57
  from glaip_sdk.cli.utils import (
57
58
  _fuzzy_pick_for_resources,
58
59
  command_hint,
@@ -278,7 +279,11 @@ class SlashSession:
278
279
  def _ensure_configuration(self) -> bool:
279
280
  """Ensure the CLI has both API URL and credentials before continuing."""
280
281
  while not self._configuration_ready():
281
- self.console.print(f"[{WARNING_STYLE}]Configuration required.[/] Launching `/login` wizard...")
282
+ self.console.print(
283
+ f"[{WARNING_STYLE}]Configuration required.[/] "
284
+ "Slash mode cannot run 'aip accounts ...'. Run setup from your terminal (e.g., "
285
+ "'aip accounts add default' or 'aip configure'), or continue with the `/login` wizard here..."
286
+ )
282
287
  self._suppress_login_layout = True
283
288
  try:
284
289
  self._cmd_login([], False)
@@ -1285,12 +1290,9 @@ class SlashSession:
1285
1290
  )
1286
1291
  )
1287
1292
 
1288
- def _get_api_url(self, config: dict[str, Any]) -> str | None:
1289
- """Get the API URL from various sources."""
1290
- api_url = None
1291
- if isinstance(self.ctx.obj, dict):
1292
- api_url = self.ctx.obj.get("api_url")
1293
- return api_url or config.get("api_url") or os.getenv("AIP_API_URL")
1293
+ def _get_api_url(self, _config: dict[str, Any] | None = None) -> str | None:
1294
+ """Get the API URL from context or account store (CLI/palette ignores env credentials)."""
1295
+ return resolve_api_url_from_context(self.ctx)
1294
1296
 
1295
1297
  def _build_agent_status_line(self, active_agent: Any | None) -> str | None:
1296
1298
  """Return a short status line about the active or recent agent."""
@@ -12,18 +12,17 @@ from __future__ import annotations
12
12
  import asyncio
13
13
  import json
14
14
  import logging
15
+ from collections.abc import Callable
15
16
  from dataclasses import dataclass
16
17
  from typing import Any
17
- from collections.abc import Callable
18
18
 
19
19
  from rich.text import Text
20
-
21
20
  from textual.app import App, ComposeResult
22
21
  from textual.binding import Binding
23
22
  from textual.containers import Container, Horizontal
24
23
  from textual.reactive import ReactiveError
25
24
  from textual.screen import ModalScreen
26
- from textual.widgets import DataTable, Footer, Header, LoadingIndicator, Static, RichLog
25
+ from textual.widgets import DataTable, Footer, Header, LoadingIndicator, RichLog, Static
27
26
 
28
27
  logger = logging.getLogger(__name__)
29
28
 
@@ -7,14 +7,13 @@ Authors:
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
- import os
11
10
  from dataclasses import dataclass
12
11
  from io import StringIO
13
12
  from typing import Any
14
13
 
15
14
  from rich.console import Console
16
15
 
17
- from glaip_sdk.cli.config import load_config
16
+ from glaip_sdk.cli.auth import resolve_api_url_from_context
18
17
  from glaip_sdk.cli.context import get_ctx_value
19
18
  from glaip_sdk.cli.transcript.cache import (
20
19
  TranscriptPayload,
@@ -118,20 +117,12 @@ def register_last_transcript(ctx: Any, payload: TranscriptPayload, store_result:
118
117
 
119
118
 
120
119
  def _resolve_api_url(ctx: Any) -> str | None:
121
- """Resolve API URL from context, environment, or config file."""
122
- api_url = get_ctx_value(ctx, "api_url")
123
- if api_url:
124
- return str(api_url)
125
-
126
- env_url = os.getenv("AIP_API_URL")
127
- if env_url:
128
- return env_url
129
-
130
- try:
131
- config = load_config()
132
- except Exception:
133
- return None
134
- return str(config.get("api_url")) if config.get("api_url") else None
120
+ """Resolve API URL from context or account store (CLI/palette ignores env creds)."""
121
+ return resolve_api_url_from_context(
122
+ ctx,
123
+ get_api_url=lambda c: get_ctx_value(c, "api_url"),
124
+ get_account_name=lambda c: get_ctx_value(c, "account_name"),
125
+ )
135
126
 
136
127
 
137
128
  def _extract_step_summaries(renderer: Any) -> list[dict[str, Any]]:
@@ -300,7 +291,10 @@ def store_transcript_for_session(
300
291
 
301
292
  meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(renderer, model)
302
293
 
303
- api_url = _resolve_api_url(ctx)
294
+ try:
295
+ api_url = _resolve_api_url(ctx)
296
+ except Exception:
297
+ api_url = None
304
298
  if api_url:
305
299
  meta["api_url"] = api_url
306
300