aisc-cli 0.1.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.
Files changed (92) hide show
  1. aisc/VERSION +1 -0
  2. aisc/__init__.py +49 -0
  3. aisc/__main__.py +6 -0
  4. aisc/adapters/__init__.py +1 -0
  5. aisc/adapters/config_reader.py +215 -0
  6. aisc/adapters/container_registry.py +718 -0
  7. aisc/adapters/data_root_store.py +296 -0
  8. aisc/adapters/docker_.py +1183 -0
  9. aisc/adapters/docker_gateway.py +1244 -0
  10. aisc/adapters/docker_sdk_backed.py +274 -0
  11. aisc/adapters/maintenance_lock.py +97 -0
  12. aisc/adapters/state_file.py +184 -0
  13. aisc/adapters/system.py +101 -0
  14. aisc/adapters/windows_config_reader.py +249 -0
  15. aisc/adapters/workspace_lease_store.py +189 -0
  16. aisc/application/__init__.py +1 -0
  17. aisc/application/artifact.py +265 -0
  18. aisc/application/bundle_fetch.py +836 -0
  19. aisc/application/cc_switch_provider.py +245 -0
  20. aisc/application/cc_switch_resolver.py +363 -0
  21. aisc/application/config_service.py +452 -0
  22. aisc/application/conversation.py +516 -0
  23. aisc/application/data_migration.py +440 -0
  24. aisc/application/data_root.py +275 -0
  25. aisc/application/docker_lifecycle.py +659 -0
  26. aisc/application/doctor.py +821 -0
  27. aisc/application/legacy_scan.py +252 -0
  28. aisc/application/network_subscription.py +642 -0
  29. aisc/application/profile_service.py +105 -0
  30. aisc/application/provider.py +92 -0
  31. aisc/application/repair.py +387 -0
  32. aisc/application/resources.py +270 -0
  33. aisc/application/runtime.py +1826 -0
  34. aisc/application/session.py +320 -0
  35. aisc/application/tool_versions.py +210 -0
  36. aisc/application/toolchain.py +91 -0
  37. aisc/application/update.py +286 -0
  38. aisc/application/usage.py +305 -0
  39. aisc/application/version.py +133 -0
  40. aisc/application/web_gateway.py +459 -0
  41. aisc/application/workspace_reconcile.py +318 -0
  42. aisc/applog.py +103 -0
  43. aisc/cli/__init__.py +1 -0
  44. aisc/cli/build_progress_parser.py +217 -0
  45. aisc/cli/commands/__init__.py +1 -0
  46. aisc/cli/commands/agents.py +117 -0
  47. aisc/cli/commands/artifact.py +210 -0
  48. aisc/cli/commands/build.py +566 -0
  49. aisc/cli/commands/bundle.py +107 -0
  50. aisc/cli/commands/cc_switch.py +158 -0
  51. aisc/cli/commands/config.py +104 -0
  52. aisc/cli/commands/container.py +787 -0
  53. aisc/cli/commands/conversation.py +72 -0
  54. aisc/cli/commands/data_root.py +84 -0
  55. aisc/cli/commands/logs.py +89 -0
  56. aisc/cli/commands/network.py +132 -0
  57. aisc/cli/commands/profile.py +52 -0
  58. aisc/cli/commands/provider.py +49 -0
  59. aisc/cli/commands/run.py +494 -0
  60. aisc/cli/commands/runs.py +125 -0
  61. aisc/cli/commands/runtime.py +450 -0
  62. aisc/cli/commands/serve.py +550 -0
  63. aisc/cli/commands/serve_fs.py +369 -0
  64. aisc/cli/commands/session.py +167 -0
  65. aisc/cli/commands/update.py +145 -0
  66. aisc/cli/commands/usage.py +74 -0
  67. aisc/cli/commands/wizard.py +130 -0
  68. aisc/cli/commands/workspaces.py +162 -0
  69. aisc/cli/help_i18n.py +385 -0
  70. aisc/cli/main.py +2926 -0
  71. aisc/cli/output.py +239 -0
  72. aisc/cli/tutorial.py +109 -0
  73. aisc/domain/__init__.py +1 -0
  74. aisc/domain/artifacts.py +279 -0
  75. aisc/domain/cc_switch_release.py +303 -0
  76. aisc/domain/config.py +64 -0
  77. aisc/domain/data_migration.py +259 -0
  78. aisc/domain/data_root.py +124 -0
  79. aisc/domain/docker_ownership.py +148 -0
  80. aisc/domain/gateway.py +168 -0
  81. aisc/domain/models.py +674 -0
  82. aisc/domain/toolchain.py +72 -0
  83. aisc/domain/web_services.py +355 -0
  84. aisc/domain/workspace_lease.py +139 -0
  85. aisc/schemas/__init__.py +1 -0
  86. aisc/schemas/config_schema.py +145 -0
  87. aisc_cli-0.1.0.dist-info/METADATA +420 -0
  88. aisc_cli-0.1.0.dist-info/RECORD +92 -0
  89. aisc_cli-0.1.0.dist-info/WHEEL +5 -0
  90. aisc_cli-0.1.0.dist-info/entry_points.txt +2 -0
  91. aisc_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  92. aisc_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,72 @@
1
+ """Conversation commands implementation (v2.1.8 T3).
2
+
3
+ Implements ``aisc conversation`` subcommands: list, preflight.
4
+ Per docs/plans/2.1.8-dev-plans/01-design.md §1b. Both are captured,
5
+ read-only commands — ``conversation resume`` deliberately does NOT exist
6
+ as a CLI command (方案 B 冻结): resume is the Workbench two-call
7
+ orchestration (preflight IPC + open_session --resume-id).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict
13
+
14
+ from aisc.application.conversation import (
15
+ delete_conversation,
16
+ list_conversations,
17
+ preflight_conversation,
18
+ rename_conversation,
19
+ )
20
+
21
+
22
+ def cmd_conversation_list(workspace: str) -> Dict[str, Any]:
23
+ """Execute ``aisc conversation list`` per design §1b."""
24
+ return list_conversations(workspace)
25
+
26
+
27
+ def cmd_conversation_preflight(workspace: str, conversation_id: str,
28
+ agent: str) -> Dict[str, Any]:
29
+ """Execute ``aisc conversation preflight`` per design §2."""
30
+ return preflight_conversation(workspace, conversation_id, agent)
31
+
32
+
33
+ def cmd_conversation_delete(workspace: str, conversation_id: str,
34
+ agent: str) -> Dict[str, Any]:
35
+ """Execute ``aisc conversation delete`` (v2.1.8 T4 手测反馈 #4)."""
36
+ return delete_conversation(workspace, conversation_id, agent)
37
+
38
+
39
+ def cmd_conversation_rename(workspace: str, conversation_id: str, agent: str,
40
+ title: str) -> Dict[str, Any]:
41
+ """Execute ``aisc conversation rename`` (v2.1.8 T4 手测反馈 #2)."""
42
+ return rename_conversation(workspace, conversation_id, agent, title)
43
+
44
+
45
+ def print_conversation_text(subcommand: str, data: Any, errors: list) -> None:
46
+ """Minimal human-readable output for ``aisc conversation`` text mode."""
47
+ if errors:
48
+ for e in errors:
49
+ print(f"Error: {e.get('message', '')}")
50
+ return
51
+ if not isinstance(data, dict):
52
+ return
53
+ if subcommand == "list":
54
+ conversations = data.get("conversations", [])
55
+ if not conversations:
56
+ print("(no conversations)")
57
+ return
58
+ for c in conversations:
59
+ line = (f"{c.get('conversation_id', '')} agent={c.get('agent', '')} "
60
+ f"{c.get('last_at') or '-'} {c.get('title', '')}")
61
+ if not c.get("resumable", True):
62
+ line += f" [unresumable: {c.get('unavailable_reason', '')}]"
63
+ print(line)
64
+ elif subcommand == "preflight":
65
+ print(f"preflight ok: {data.get('conversation_id', '')} "
66
+ f"agent={data.get('agent', '')}")
67
+ elif subcommand == "delete":
68
+ print(f"deleted: {data.get('conversation_id', '')} "
69
+ f"agent={data.get('agent', '')}")
70
+ elif subcommand == "rename":
71
+ print(f"renamed: {data.get('conversation_id', '')} "
72
+ f"agent={data.get('agent', '')} title={data.get('title', '')}")
@@ -0,0 +1,84 @@
1
+ """``aisc data-root`` commands (Stage 7, 7d) — doctor / migrate / rollback.
2
+
3
+ Non-interactive by contract (03-ux-flow): conflicts and unconsented unknowns
4
+ exit non-zero via CliError instead of guessing. All output is
5
+ JSON-serializable for the aisc.cli/v1 envelope; text mode prints a short
6
+ summary.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional
13
+
14
+ from aisc.adapters.data_root_store import DataRootStore
15
+ from aisc.application.data_migration import MigrationExecutor
16
+ from aisc.application.data_root import DataRootResolver
17
+
18
+
19
+ def _executor(workspace: Optional[str]) -> MigrationExecutor:
20
+ ws = Path(workspace).resolve() if workspace else Path.cwd()
21
+ resolved = DataRootResolver().resolve(ws)
22
+ return MigrationExecutor(ws, resolved, store=DataRootStore(resolved))
23
+
24
+
25
+ def cmd_data_root_doctor(workspace: Optional[str] = None) -> Dict[str, Any]:
26
+ return _executor(workspace).doctor()
27
+
28
+
29
+ def cmd_data_root_migrate(
30
+ workspace: Optional[str] = None,
31
+ *,
32
+ dry_run: bool = False,
33
+ quarantine_unknown: bool = False,
34
+ ) -> Dict[str, Any]:
35
+ executor = _executor(workspace)
36
+ if dry_run:
37
+ return executor.dry_run()
38
+ return executor.migrate(quarantine_unknown=quarantine_unknown).to_dict()
39
+
40
+
41
+ def cmd_data_root_rollback(
42
+ workspace: Optional[str] = None,
43
+ manifest: Optional[str] = None,
44
+ ) -> Dict[str, Any]:
45
+ return _executor(workspace).rollback(
46
+ manifest_path=Path(manifest) if manifest else None
47
+ ).to_dict()
48
+
49
+
50
+ def print_data_root_text(data: Any) -> None:
51
+ """Minimal human-readable output for text mode."""
52
+ if not isinstance(data, dict):
53
+ return
54
+ if "legacy" in data and "data_root" in data:
55
+ root = data["data_root"]
56
+ print(f"data root : {root.get('root', '')} ({root.get('origin')})")
57
+ print(f"writable : {root.get('writable')}")
58
+ print(f"ws hash : {root.get('workspace_hash', '')}")
59
+ legacy = data.get("legacy") or {}
60
+ counts = legacy.get("counts") or {}
61
+ print("legacy : "
62
+ f"owned={counts.get('owned', 0)} transient={counts.get('transient', 0)} "
63
+ f"unknown={counts.get('unknown', 0)} conflict={counts.get('conflict', 0)}")
64
+ pending = data.get("pending_manifest")
65
+ if pending:
66
+ print(f"manifest : {pending}")
67
+ return
68
+ if "plan" in data:
69
+ print(f"dry-run : {data['plan'].get('copy_count', 0)} file(s) to copy, "
70
+ f"~{data['plan'].get('owned_bytes', 0)} bytes")
71
+ for conflict in data.get("conflicts", []):
72
+ print(f"CONFLICT : {conflict}")
73
+ for unknown in data.get("unknowns", []):
74
+ print(f"UNKNOWN : {unknown}")
75
+ return
76
+ outcome = data.get("outcome")
77
+ if outcome == "rolled_back":
78
+ print(f"rolled back: removed={data.get('removed', 0)} "
79
+ f"kept={data.get('kept', 0)} restored={data.get('restored', 0)}")
80
+ else:
81
+ print(f"migration {outcome}: copied={data.get('copied', 0)} "
82
+ f"skipped={data.get('skipped', 0)} quarantined={data.get('quarantined', 0)}")
83
+ if data.get("markers"):
84
+ print(f"markers : {', '.join(data['markers'])}")
@@ -0,0 +1,89 @@
1
+ """``aisc logs`` command layer (lifecycle-logging P2).
2
+
3
+ Read-only views over the shared JSONL timeline
4
+ (``<data root>/logs/aisc.log``): `show` returns the recent event tail
5
+ (filtered by writer), `path` prints the file location. Lines are secret-free
6
+ by construction (P1's allowlisted schema) — this layer adds no redaction
7
+ because there is nothing sensitive to redact.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from typing import Any, Dict, List
14
+
15
+ from aisc.applog import log_file_path
16
+
17
+
18
+ def cmd_logs_show(args: Any) -> Dict[str, Any]:
19
+ lines = int(getattr(args, "lines", 200) or 200)
20
+ source = getattr(args, "source", "all") or "all"
21
+ path = log_file_path()
22
+ events: List[Dict[str, Any]] = []
23
+ if path is not None and path.exists():
24
+ # Current file only — rotated (.1/.2) stay cold history. The file is
25
+ # size-capped (≤2MB) so a full read is cheap.
26
+ raw = path.read_text(encoding="utf-8", errors="replace")
27
+ for line in raw.splitlines():
28
+ if not line.strip():
29
+ continue
30
+ try:
31
+ rec = json.loads(line)
32
+ except ValueError:
33
+ continue # a torn line (crash mid-append) never breaks the view
34
+ if not isinstance(rec, dict):
35
+ continue
36
+ if source != "all" and rec.get("source") != source:
37
+ continue
38
+ events.append(rec)
39
+ return {
40
+ "path": str(path) if path is not None else None,
41
+ "source": source,
42
+ "returned": len(events[-lines:]),
43
+ "total_matched": len(events),
44
+ "lines": events[-lines:],
45
+ }
46
+
47
+
48
+ def cmd_logs_path(args: Any) -> Dict[str, Any]:
49
+ path = log_file_path()
50
+ return {
51
+ "path": str(path) if path is not None else None,
52
+ "exists": bool(path is not None and path.exists()),
53
+ }
54
+
55
+
56
+ def print_logs_text(data: Dict[str, Any], is_show: bool) -> None:
57
+ """Human-readable output."""
58
+ if not isinstance(data, dict):
59
+ return
60
+ path = data.get("path")
61
+ if not is_show:
62
+ print(path or "(log location unresolvable — data root error)")
63
+ return
64
+ if path is None:
65
+ print("(log location unresolvable — data root error)")
66
+ return
67
+ lines = data.get("lines") or []
68
+ if not lines:
69
+ print(f"No events recorded yet ({path})")
70
+ return
71
+ for rec in lines:
72
+ ts = str(rec.get("ts", ""))[:19]
73
+ level = str(rec.get("level", "?")).upper()
74
+ src = rec.get("source", "?")
75
+ event = rec.get("event", "?")
76
+ extras = []
77
+ if rec.get("run_id"):
78
+ extras.append(f"run={str(rec['run_id'])[:13]}")
79
+ for key in ("action", "command", "phase", "container", "outcome",
80
+ "exit_code", "duration_ms", "error_code", "state",
81
+ "detail"):
82
+ if key in rec and rec[key] is not None:
83
+ extras.append(f"{key}={rec[key]}")
84
+ suffix = " ".join(extras)
85
+ print(f"{ts} {level:<5} {src:<3} {event} {suffix}".rstrip())
86
+ shown = data.get("returned", len(lines))
87
+ total = data.get("total_matched", len(lines))
88
+ if shown < total:
89
+ print(f"-- showing {shown} of {total} matched events ({path})")
@@ -0,0 +1,132 @@
1
+ """``aisc network subscription`` command layer (IDEA-2, 2b).
2
+
3
+ Thin wrappers over ``aisc.application.network_subscription``. The
4
+ subscription URL is a credential: it rides stdin (``import`` reads one URL
5
+ line; ``import-file`` reads the full subscription content), never argv.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import Any, Dict
12
+
13
+ from aisc.domain.models import CliError
14
+
15
+
16
+ def _read_stdin_text(*, what: str) -> str:
17
+ raw = sys.stdin.read()
18
+ if not raw.strip():
19
+ raise CliError(
20
+ message=f"{what} on stdin is required (secrets never ride argv)",
21
+ exit_code=2,
22
+ error_code="AISC_ERR_USAGE",
23
+ )
24
+ return raw
25
+
26
+
27
+ def cmd_network_subscription_import(args: Any) -> Dict[str, Any]:
28
+ from aisc.application.network_subscription import import_subscription
29
+
30
+ url = _read_stdin_text(what="a subscription URL").strip()
31
+ return import_subscription(url)
32
+
33
+
34
+ def cmd_network_subscription_import_file(args: Any) -> Dict[str, Any]:
35
+ from aisc.application.network_subscription import import_subscription_content
36
+
37
+ content = sys.stdin.buffer.read()
38
+ return import_subscription_content(content)
39
+
40
+
41
+ def cmd_network_subscription_store_downloaded(args: Any) -> Dict[str, Any]:
42
+ """挂账①: stdin carries a JSON document {url, content_b64, userinfo} —
43
+ the body of a Rust-side (reqwest) download. b64 keeps the payload
44
+ byte-exact through the text stdin channel."""
45
+ import base64
46
+ import json
47
+
48
+ from aisc.application.network_subscription import store_downloaded
49
+
50
+ raw = sys.stdin.read()
51
+ try:
52
+ request = json.loads(raw) if raw.strip() else {}
53
+ except json.JSONDecodeError as exc:
54
+ raise CliError(
55
+ message=f"store-downloaded request is not valid JSON: {exc}",
56
+ exit_code=2,
57
+ error_code="AISC_ERR_USAGE",
58
+ ) from exc
59
+ if not isinstance(request, dict):
60
+ raise CliError(
61
+ message="store-downloaded request must be a JSON object",
62
+ exit_code=2,
63
+ error_code="AISC_ERR_USAGE",
64
+ )
65
+ b64 = str(request.get("content_b64") or "")
66
+ try:
67
+ content = base64.b64decode(b64, validate=True) if b64 else b""
68
+ except Exception as exc:
69
+ raise CliError(
70
+ message=f"content_b64 is not valid base64: {exc}",
71
+ exit_code=2,
72
+ error_code="AISC_ERR_USAGE",
73
+ ) from exc
74
+ url = request.get("url")
75
+ userinfo = request.get("userinfo")
76
+ return store_downloaded(
77
+ str(url) if isinstance(url, str) and url else None,
78
+ content,
79
+ str(userinfo) if isinstance(userinfo, str) else "",
80
+ )
81
+
82
+
83
+ def cmd_network_subscription_refresh(args: Any) -> Dict[str, Any]:
84
+ from aisc.application.network_subscription import refresh_subscription
85
+
86
+ return refresh_subscription()
87
+
88
+
89
+ def cmd_network_subscription_show(args: Any) -> Dict[str, Any]:
90
+ from aisc.application.network_subscription import show_subscription
91
+
92
+ return show_subscription()
93
+
94
+
95
+ def cmd_network_subscription_clear(args: Any) -> Dict[str, Any]:
96
+ from aisc.application.network_subscription import clear_subscription
97
+
98
+ if not getattr(args, "confirm", False):
99
+ raise CliError(
100
+ message="clear requires --confirm",
101
+ exit_code=2,
102
+ error_code="AISC_ERR_USAGE",
103
+ )
104
+ return clear_subscription()
105
+
106
+
107
+ def print_network_subscription_text(data: Dict[str, Any]) -> None:
108
+ """Human-readable output (secret-free: masked URL only)."""
109
+ if not isinstance(data, dict):
110
+ return
111
+ if not data.get("configured"):
112
+ print("No subscription configured.")
113
+ print("Import one: aisc network subscription import (URL on stdin)")
114
+ print(" or: aisc network subscription import-file (content on stdin)")
115
+ return
116
+ print(f"Source: {data.get('source') or 'manual'}")
117
+ print(f"URL: {data.get('url_masked') or '(manually imported)'}")
118
+ print(f"Updated: {data.get('fetched_at') or '(unknown)'}")
119
+ print(f"Config: {data.get('config_path') or ''} ({data.get('config_sha256') or '?'})")
120
+ userinfo = data.get("userinfo")
121
+ if isinstance(userinfo, dict) and userinfo:
122
+ total = userinfo.get("total")
123
+ used = userinfo.get("upload", 0) + userinfo.get("download", 0)
124
+ if total:
125
+ pct = min(100.0, used * 100.0 / total)
126
+ print(f"Usage: {used} / {total} bytes ({pct:.1f}%)")
127
+ else:
128
+ print(f"Usage: {used} bytes (unlimited plan)")
129
+ if "expire" in userinfo:
130
+ print(f"Expires: epoch {userinfo['expire']}")
131
+ else:
132
+ print("Usage: (subscription provided no usage header)")
@@ -0,0 +1,52 @@
1
+ """CLI command implementations for ``aisc profile list`` and ``aisc profile show``.
2
+
3
+ Read-only — returns built-in safe/unsafe profiles only.
4
+ No user-defined profiles, no confirmation flow, no run integration.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict
10
+
11
+ from aisc.application.profile_service import (
12
+ run_profile_list, ProfileListResult,
13
+ run_profile_show, ProfileShowResult,
14
+ )
15
+
16
+
17
+ def cmd_profile_list() -> ProfileListResult:
18
+ """Execute ``profile list``."""
19
+ return run_profile_list()
20
+
21
+
22
+ def cmd_profile_show(name: str) -> ProfileShowResult:
23
+ """Execute ``profile show NAME``."""
24
+ return run_profile_show(name)
25
+
26
+
27
+ def print_profile_list_text(data: Dict[str, Any]) -> None:
28
+ """Print profile list in human-readable format."""
29
+ profiles = data.get("profiles", [])
30
+ print("=== Profile List ===")
31
+ print()
32
+ if not profiles:
33
+ print("No profiles found.")
34
+ else:
35
+ for p in profiles:
36
+ print(f" {p['name']:12s} dangerously_skip_permissions={p['dangerously_skip_permissions']}")
37
+ print(f" {p['description']}")
38
+ print()
39
+ print()
40
+
41
+
42
+ def print_profile_show_text(data: Dict[str, Any]) -> None:
43
+ """Print a single profile in human-readable format."""
44
+ if not data:
45
+ print("Profile not found.")
46
+ return
47
+ print("=== Profile Detail ===")
48
+ print()
49
+ for key in ("name", "description"):
50
+ print(f" {key:30s} {data.get(key, '')}")
51
+ print(f" dangerously_skip_permissions {data.get('dangerously_skip_permissions', False)}")
52
+ print()
@@ -0,0 +1,49 @@
1
+ """Provider command implementation (Workbench Phase 0 S0.4).
2
+
3
+ Implements ``aisc provider current``. The legacy interactive ``set-key``
4
+ subcommand stays in ``aisc.cli.commands.container`` / ``main.py``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ from aisc.adapters.docker_ import DockerExecutor
13
+ from aisc.adapters.docker_sdk_backed import default_executor
14
+ from aisc.application.provider import current_provider
15
+
16
+
17
+ def _resolve_workspace_and_registry(workspace: Optional[str]):
18
+ from aisc.application.data_root import workspace_state_dir
19
+
20
+ ws_path = Path(workspace).resolve() if workspace else Path.cwd()
21
+ return str(ws_path), workspace_state_dir(ws_path)
22
+
23
+
24
+ def cmd_provider_current(
25
+ runtime_id: str,
26
+ agent: str,
27
+ workspace: Optional[str] = None,
28
+ executor: Optional[DockerExecutor] = None,
29
+ ) -> Dict[str, Any]:
30
+ """Execute ``aisc provider current`` per contract §七."""
31
+ exec_ = executor or default_executor()
32
+ _ws, reg_root = _resolve_workspace_and_registry(workspace)
33
+ status = current_provider(
34
+ runtime_id=runtime_id,
35
+ agent=agent,
36
+ executor=exec_,
37
+ registry_root=reg_root,
38
+ )
39
+ return status.to_dict()
40
+
41
+
42
+ def print_provider_current_text(data: Any) -> None:
43
+ """Minimal human-readable output for ``aisc provider current`` text mode."""
44
+ if not isinstance(data, dict):
45
+ return
46
+ print(f"{data.get('agent', '')}: {data.get('provider_name', '')} "
47
+ f"({data.get('provider_id', '')})")
48
+ print(f" route_mode : {data.get('route_mode', '')}")
49
+ print(f" auth_status: {data.get('auth_status', '')}")