applied-cli 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {applied_cli-0.1.0 → applied_cli-0.2.0}/PKG-INFO +7 -3
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/auth.py +323 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/mcp_server.py +63 -1
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli.egg-info/PKG-INFO +7 -3
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli.egg-info/requires.txt +5 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/pyproject.toml +8 -3
- {applied_cli-0.1.0 → applied_cli-0.2.0}/README.md +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/__init__.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/auth_store.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/__init__.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/_hints.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/_normalize.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/_parsers.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/_ui.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/agent.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/chat.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/coverage.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/discover.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/fix.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/insights.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/intents.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/rate.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/responses.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/shop.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/simulate.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/commands/spec.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/config.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/error_reporting.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/http.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/main.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/presets/demo.yaml +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/runtime.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/shop_spec.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli/spec_workflow.py +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/setup.cfg +0 -0
- {applied_cli-0.1.0 → applied_cli-0.2.0}/tests/test_parsers_and_insights.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI and MCP server for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/AppliedLabsAI/applied-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/AppliedLabsAI/applied-cli
|
|
9
9
|
Keywords: applied-labs,ai-agents,support,mcp,claude
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
@@ -19,6 +19,10 @@ Requires-Dist: pyyaml>=6.0
|
|
|
19
19
|
Requires-Dist: typer>=0.16.0
|
|
20
20
|
Provides-Extra: mcp
|
|
21
21
|
Requires-Dist: mcp>=1.2.0; extra == "mcp"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
25
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
22
26
|
|
|
23
27
|
# Applied Labs CLI
|
|
24
28
|
|
|
@@ -720,6 +720,329 @@ def use_shop(
|
|
|
720
720
|
typer.echo("To switch again: applied-cli auth use-shop \"<shop name or uuid>\"")
|
|
721
721
|
|
|
722
722
|
|
|
723
|
+
@app.command("login-start")
|
|
724
|
+
def login_start(
|
|
725
|
+
endpoint: Optional[str] = typer.Option(
|
|
726
|
+
None,
|
|
727
|
+
"--endpoint",
|
|
728
|
+
help="Endpoint alias or URL: prod, dev, local, or full host.",
|
|
729
|
+
envvar="APPLIED_ENDPOINT",
|
|
730
|
+
),
|
|
731
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
732
|
+
# Hidden options
|
|
733
|
+
base_url: Optional[str] = typer.Option(None, envvar="APPLIED_BASE_URL", hidden=True),
|
|
734
|
+
timeout: float = typer.Option(10.0, hidden=True),
|
|
735
|
+
) -> None:
|
|
736
|
+
"""Start device login flow and return approval URL (non-blocking).
|
|
737
|
+
|
|
738
|
+
This command starts the OAuth device flow and immediately returns the
|
|
739
|
+
approval URL and device code. Use `login-complete` to finish authentication
|
|
740
|
+
after the user approves in their browser.
|
|
741
|
+
|
|
742
|
+
Example:
|
|
743
|
+
applied-cli auth login-start --json
|
|
744
|
+
# User visits URL and approves
|
|
745
|
+
applied-cli auth login-complete --device-code <code> --json
|
|
746
|
+
"""
|
|
747
|
+
resolved_base_url = _resolve_base_url_for_auth(
|
|
748
|
+
endpoint=endpoint,
|
|
749
|
+
base_url=base_url,
|
|
750
|
+
existing=None,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
try:
|
|
754
|
+
device_start = start_cli_device_login(
|
|
755
|
+
base_url=resolved_base_url,
|
|
756
|
+
timeout_seconds=timeout,
|
|
757
|
+
)
|
|
758
|
+
except APIError as exc:
|
|
759
|
+
if exc.status_code == 404:
|
|
760
|
+
if output_json:
|
|
761
|
+
typer.echo(
|
|
762
|
+
json.dumps(
|
|
763
|
+
{
|
|
764
|
+
"success": False,
|
|
765
|
+
"error": "endpoint_not_found",
|
|
766
|
+
"message": f"Device login endpoint not found at {resolved_base_url}.",
|
|
767
|
+
"hint": "Specify an endpoint: --endpoint prod or --endpoint local",
|
|
768
|
+
},
|
|
769
|
+
indent=2,
|
|
770
|
+
),
|
|
771
|
+
err=True,
|
|
772
|
+
)
|
|
773
|
+
else:
|
|
774
|
+
typer.echo(
|
|
775
|
+
f"Device login endpoint not found at {resolved_base_url}.\n"
|
|
776
|
+
"Try: applied-cli auth login-start --endpoint prod",
|
|
777
|
+
err=True,
|
|
778
|
+
)
|
|
779
|
+
raise typer.Exit(code=1) from exc
|
|
780
|
+
typer.echo(render_api_error(exc, action="start device login"), err=True)
|
|
781
|
+
raise typer.Exit(code=1) from exc
|
|
782
|
+
|
|
783
|
+
approval_url = str(
|
|
784
|
+
device_start.get("verification_uri_complete")
|
|
785
|
+
or device_start.get("verification_uri")
|
|
786
|
+
or device_start.get("verification_url")
|
|
787
|
+
or ""
|
|
788
|
+
).strip()
|
|
789
|
+
device_code = str(device_start.get("device_code") or "").strip()
|
|
790
|
+
user_code = str(device_start.get("user_code") or "").strip()
|
|
791
|
+
expires_in = int(device_start.get("expires_in") or 180)
|
|
792
|
+
|
|
793
|
+
if not device_code:
|
|
794
|
+
err_msg = "Device login start response missing device code."
|
|
795
|
+
if output_json:
|
|
796
|
+
typer.echo(
|
|
797
|
+
json.dumps({"success": False, "error": "invalid_response", "message": err_msg}),
|
|
798
|
+
err=True,
|
|
799
|
+
)
|
|
800
|
+
else:
|
|
801
|
+
typer.echo(err_msg, err=True)
|
|
802
|
+
raise typer.Exit(code=1)
|
|
803
|
+
|
|
804
|
+
result = {
|
|
805
|
+
"success": True,
|
|
806
|
+
"status": "pending_approval",
|
|
807
|
+
"approval_url": approval_url,
|
|
808
|
+
"device_code": device_code,
|
|
809
|
+
"user_code": user_code or None,
|
|
810
|
+
"expires_in": expires_in,
|
|
811
|
+
"endpoint": resolved_base_url,
|
|
812
|
+
"next_step": "Visit the approval URL, then run: applied-cli auth login-complete --device-code <device_code>",
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if output_json:
|
|
816
|
+
typer.echo(json.dumps(result, indent=2))
|
|
817
|
+
else:
|
|
818
|
+
typer.echo(f"Approval URL: {approval_url}")
|
|
819
|
+
if user_code:
|
|
820
|
+
typer.echo(f"User code: {user_code}")
|
|
821
|
+
typer.echo(f"Device code: {device_code}")
|
|
822
|
+
typer.echo(f"Expires in: {expires_in} seconds")
|
|
823
|
+
typer.echo("")
|
|
824
|
+
typer.echo("After approving in browser, run:")
|
|
825
|
+
typer.echo(f" applied-cli auth login-complete --device-code {device_code}")
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@app.command("login-complete")
|
|
829
|
+
def login_complete(
|
|
830
|
+
device_code: str = typer.Option(..., "--device-code", help="Device code from login-start."),
|
|
831
|
+
endpoint: Optional[str] = typer.Option(
|
|
832
|
+
None,
|
|
833
|
+
"--endpoint",
|
|
834
|
+
help="Endpoint alias or URL (must match login-start).",
|
|
835
|
+
envvar="APPLIED_ENDPOINT",
|
|
836
|
+
),
|
|
837
|
+
shop_id: Optional[str] = typer.Option(
|
|
838
|
+
None, "--shop-id", help="Pre-select a shop UUID.", envvar="APPLIED_SHOP_ID"
|
|
839
|
+
),
|
|
840
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
841
|
+
# Hidden options
|
|
842
|
+
profile: Optional[str] = typer.Option(None, "--profile", envvar="APPLIED_PROFILE", hidden=True),
|
|
843
|
+
base_url: Optional[str] = typer.Option(None, envvar="APPLIED_BASE_URL", hidden=True),
|
|
844
|
+
timeout: float = typer.Option(10.0, hidden=True),
|
|
845
|
+
max_attempts: int = typer.Option(3, hidden=True),
|
|
846
|
+
poll_interval: float = typer.Option(2.0, hidden=True),
|
|
847
|
+
) -> None:
|
|
848
|
+
"""Complete device login after user approves in browser.
|
|
849
|
+
|
|
850
|
+
Polls the API to check if the user has approved the login request.
|
|
851
|
+
If approved, saves credentials and selects a shop.
|
|
852
|
+
|
|
853
|
+
Example:
|
|
854
|
+
applied-cli auth login-complete --device-code abc123 --json
|
|
855
|
+
"""
|
|
856
|
+
profile_name = _resolve_profile_name(profile)
|
|
857
|
+
resolved_base_url = _resolve_base_url_for_auth(
|
|
858
|
+
endpoint=endpoint,
|
|
859
|
+
base_url=base_url,
|
|
860
|
+
existing=None,
|
|
861
|
+
)
|
|
862
|
+
explicit_shop_id = shop_id or os.getenv("APPLIED_SHOP_ID")
|
|
863
|
+
|
|
864
|
+
# Poll for approval (with limited attempts for non-blocking behavior)
|
|
865
|
+
resolved_token = None
|
|
866
|
+
resolved_shop_id = explicit_shop_id or ""
|
|
867
|
+
resolved_shop_name = ""
|
|
868
|
+
|
|
869
|
+
for attempt in range(max_attempts):
|
|
870
|
+
try:
|
|
871
|
+
poll = poll_cli_device_login(
|
|
872
|
+
base_url=resolved_base_url,
|
|
873
|
+
device_code=device_code,
|
|
874
|
+
timeout_seconds=timeout,
|
|
875
|
+
)
|
|
876
|
+
except APIError as exc:
|
|
877
|
+
if output_json:
|
|
878
|
+
typer.echo(
|
|
879
|
+
json.dumps(
|
|
880
|
+
{"success": False, "error": "poll_failed", "message": str(exc)},
|
|
881
|
+
indent=2,
|
|
882
|
+
),
|
|
883
|
+
err=True,
|
|
884
|
+
)
|
|
885
|
+
else:
|
|
886
|
+
typer.echo(render_api_error(exc, action="poll device login"), err=True)
|
|
887
|
+
raise typer.Exit(code=1) from exc
|
|
888
|
+
|
|
889
|
+
status_value = str(poll.get("status") or "pending").strip().lower()
|
|
890
|
+
|
|
891
|
+
if status_value == "approved":
|
|
892
|
+
resolved_token = str(poll.get("api_token") or "").strip()
|
|
893
|
+
discovered_shop_id = str(poll.get("shop_id") or "").strip()
|
|
894
|
+
if discovered_shop_id and not resolved_shop_id:
|
|
895
|
+
resolved_shop_id = discovered_shop_id
|
|
896
|
+
break
|
|
897
|
+
elif status_value in {"expired", "denied"}:
|
|
898
|
+
err_result = {
|
|
899
|
+
"success": False,
|
|
900
|
+
"error": f"login_{status_value}",
|
|
901
|
+
"message": f"Device login {status_value}.",
|
|
902
|
+
"hint": "Run `applied-cli auth login-start` again to get a new approval URL.",
|
|
903
|
+
}
|
|
904
|
+
if output_json:
|
|
905
|
+
typer.echo(json.dumps(err_result, indent=2), err=True)
|
|
906
|
+
else:
|
|
907
|
+
typer.echo(f"Device login {status_value}. Start a new login flow.", err=True)
|
|
908
|
+
raise typer.Exit(code=1)
|
|
909
|
+
else:
|
|
910
|
+
# Still pending
|
|
911
|
+
if attempt < max_attempts - 1:
|
|
912
|
+
time.sleep(poll_interval)
|
|
913
|
+
|
|
914
|
+
if not resolved_token:
|
|
915
|
+
pending_result = {
|
|
916
|
+
"success": False,
|
|
917
|
+
"status": "pending",
|
|
918
|
+
"message": "Login not yet approved. User must visit the approval URL first.",
|
|
919
|
+
"hint": "Wait for user to approve in browser, then retry this command.",
|
|
920
|
+
}
|
|
921
|
+
if output_json:
|
|
922
|
+
typer.echo(json.dumps(pending_result, indent=2))
|
|
923
|
+
else:
|
|
924
|
+
typer.echo("Login not yet approved. Please approve in browser first.")
|
|
925
|
+
raise typer.Exit(code=1)
|
|
926
|
+
|
|
927
|
+
# Resolve shop name if we have an ID
|
|
928
|
+
if resolved_shop_id and not resolved_shop_name:
|
|
929
|
+
try:
|
|
930
|
+
shops_for_name = list_accessible_shops(
|
|
931
|
+
base_url=resolved_base_url,
|
|
932
|
+
api_token=resolved_token,
|
|
933
|
+
timeout_seconds=timeout,
|
|
934
|
+
)
|
|
935
|
+
name_match = next(
|
|
936
|
+
(r for r in shops_for_name if str(r.get("id")) == resolved_shop_id), None
|
|
937
|
+
)
|
|
938
|
+
if name_match:
|
|
939
|
+
resolved_shop_name = str(name_match.get("name") or "")
|
|
940
|
+
except APIError:
|
|
941
|
+
pass
|
|
942
|
+
|
|
943
|
+
# If no shop_id yet, try to auto-select (single shop) or require explicit selection
|
|
944
|
+
if not resolved_shop_id:
|
|
945
|
+
try:
|
|
946
|
+
shops = list_accessible_shops(
|
|
947
|
+
base_url=resolved_base_url,
|
|
948
|
+
api_token=resolved_token,
|
|
949
|
+
timeout_seconds=timeout,
|
|
950
|
+
)
|
|
951
|
+
except APIError:
|
|
952
|
+
shops = []
|
|
953
|
+
|
|
954
|
+
if shops:
|
|
955
|
+
if len(shops) == 1 and shops[0].get("id"):
|
|
956
|
+
resolved_shop_id = str(shops[0]["id"])
|
|
957
|
+
resolved_shop_name = str(shops[0].get("name") or "")
|
|
958
|
+
elif output_json:
|
|
959
|
+
# Multiple shops - return them for selection
|
|
960
|
+
typer.echo(
|
|
961
|
+
json.dumps(
|
|
962
|
+
{
|
|
963
|
+
"success": False,
|
|
964
|
+
"error": "shop_selection_required",
|
|
965
|
+
"message": "Multiple shops available. Specify --shop-id.",
|
|
966
|
+
"available_shops": shops,
|
|
967
|
+
},
|
|
968
|
+
indent=2,
|
|
969
|
+
)
|
|
970
|
+
)
|
|
971
|
+
raise typer.Exit(code=1)
|
|
972
|
+
else:
|
|
973
|
+
resolved_shop_id, resolved_shop_name = _select_shop_interactively(shops)
|
|
974
|
+
|
|
975
|
+
if not resolved_shop_id:
|
|
976
|
+
err_result = {
|
|
977
|
+
"success": False,
|
|
978
|
+
"error": "missing_shop_id",
|
|
979
|
+
"message": "Could not determine shop. Pass --shop-id explicitly.",
|
|
980
|
+
}
|
|
981
|
+
if output_json:
|
|
982
|
+
typer.echo(json.dumps(err_result, indent=2), err=True)
|
|
983
|
+
else:
|
|
984
|
+
typer.echo("Could not determine shop. Pass --shop-id.", err=True)
|
|
985
|
+
raise typer.Exit(code=1)
|
|
986
|
+
|
|
987
|
+
# Validate token against shop
|
|
988
|
+
try:
|
|
989
|
+
validate_api_token(
|
|
990
|
+
base_url=resolved_base_url,
|
|
991
|
+
shop_id=resolved_shop_id,
|
|
992
|
+
api_token=resolved_token,
|
|
993
|
+
timeout_seconds=timeout,
|
|
994
|
+
)
|
|
995
|
+
except APIError as exc:
|
|
996
|
+
if output_json:
|
|
997
|
+
typer.echo(
|
|
998
|
+
json.dumps(
|
|
999
|
+
{
|
|
1000
|
+
"success": False,
|
|
1001
|
+
"error": "validation_failed",
|
|
1002
|
+
"message": str(exc),
|
|
1003
|
+
},
|
|
1004
|
+
indent=2,
|
|
1005
|
+
),
|
|
1006
|
+
err=True,
|
|
1007
|
+
)
|
|
1008
|
+
else:
|
|
1009
|
+
typer.echo(render_api_error(exc, action="validate token"), err=True)
|
|
1010
|
+
raise typer.Exit(code=1) from exc
|
|
1011
|
+
|
|
1012
|
+
# Save credentials
|
|
1013
|
+
save_credentials(
|
|
1014
|
+
Credentials(
|
|
1015
|
+
base_url=resolved_base_url,
|
|
1016
|
+
shop_id=resolved_shop_id,
|
|
1017
|
+
api_token=resolved_token,
|
|
1018
|
+
),
|
|
1019
|
+
profile=profile_name,
|
|
1020
|
+
set_active=True,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
shop_label = (
|
|
1024
|
+
f"{resolved_shop_name} ({resolved_shop_id})" if resolved_shop_name else resolved_shop_id
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
if output_json:
|
|
1028
|
+
result: dict = {
|
|
1029
|
+
"success": True,
|
|
1030
|
+
"logged_in": True,
|
|
1031
|
+
"endpoint": resolved_base_url,
|
|
1032
|
+
"shop_id": resolved_shop_id,
|
|
1033
|
+
"token_masked": mask_token(resolved_token),
|
|
1034
|
+
}
|
|
1035
|
+
if resolved_shop_name:
|
|
1036
|
+
result["shop_name"] = resolved_shop_name
|
|
1037
|
+
typer.echo(json.dumps(result, indent=2))
|
|
1038
|
+
return
|
|
1039
|
+
|
|
1040
|
+
typer.echo("Login successful!")
|
|
1041
|
+
typer.echo(f"- shop: {shop_label}")
|
|
1042
|
+
typer.echo(f"- endpoint: {resolved_base_url}")
|
|
1043
|
+
typer.echo(f"- token: {mask_token(resolved_token)}")
|
|
1044
|
+
|
|
1045
|
+
|
|
723
1046
|
@app.command("logout")
|
|
724
1047
|
def logout(
|
|
725
1048
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
@@ -22,11 +22,33 @@ from mcp.server.fastmcp import FastMCP
|
|
|
22
22
|
mcp = FastMCP(
|
|
23
23
|
"applied-cli",
|
|
24
24
|
instructions="""
|
|
25
|
-
You have access to the Applied Labs
|
|
25
|
+
You have access to the Applied Labs plugin for managing AI support agents.
|
|
26
|
+
|
|
27
|
+
IMPORTANT: Never tell users to run CLI commands. Always use these MCP tools directly.
|
|
28
|
+
|
|
29
|
+
## Authentication Flow
|
|
30
|
+
|
|
31
|
+
When a user is not logged in, YOU must handle authentication using these tools:
|
|
32
|
+
|
|
33
|
+
1. Call `auth_login()` → Returns JSON with `approval_url` and `device_code`
|
|
34
|
+
2. Tell user: "Please visit this URL to authorize: [approval_url]"
|
|
35
|
+
3. Ask user to confirm when they've approved
|
|
36
|
+
4. Call `auth_login_complete(device_code="...")` with the device_code from step 1
|
|
37
|
+
5. Verify with `auth_status()`
|
|
38
|
+
|
|
39
|
+
Example response when not logged in:
|
|
40
|
+
"You need to authenticate. Please visit this URL to authorize:
|
|
41
|
+
https://appliedlabs.ai/cli/authorize?code=XXXX
|
|
42
|
+
|
|
43
|
+
Let me know when you've approved, and I'll complete the login."
|
|
44
|
+
|
|
45
|
+
DO NOT tell users to run `applied-cli auth login` or any other CLI command.
|
|
26
46
|
|
|
27
47
|
## Quick Reference
|
|
28
48
|
|
|
29
49
|
### Setup & Auth
|
|
50
|
+
- `auth_login` - Start login flow (returns approval URL for user)
|
|
51
|
+
- `auth_login_complete` - Complete login after user approves
|
|
30
52
|
- `auth_status` - Check authentication and current shop
|
|
31
53
|
- `auth_shops` / `auth_switch_shop` - List and switch shops
|
|
32
54
|
- `shop_setup` - Full shop bootstrap from YAML spec
|
|
@@ -106,6 +128,46 @@ def _run_cli(args: list[str], timeout: int = 120) -> dict[str, Any]:
|
|
|
106
128
|
# =============================================================================
|
|
107
129
|
|
|
108
130
|
|
|
131
|
+
@mcp.tool()
|
|
132
|
+
async def auth_login(endpoint: str = "prod") -> str:
|
|
133
|
+
"""
|
|
134
|
+
Start the login flow by initiating device authentication.
|
|
135
|
+
|
|
136
|
+
Returns an approval URL that the user must visit in their browser to authorize.
|
|
137
|
+
After the user approves, call `auth_login_complete` with the device_code to finish.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
endpoint: API endpoint - 'prod', 'dev', 'local', or full URL (default: prod)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
JSON with approval_url, device_code, and user_code for the user to complete login.
|
|
144
|
+
"""
|
|
145
|
+
result = _run_cli(["auth", "login-start", "--endpoint", endpoint, "--json"])
|
|
146
|
+
return json.dumps(result, indent=2)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@mcp.tool()
|
|
150
|
+
async def auth_login_complete(device_code: str, endpoint: str = "prod", shop_id: str = None) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Complete the login flow after user has approved in browser.
|
|
153
|
+
|
|
154
|
+
Call this after the user has visited the approval URL from `auth_login`.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
device_code: The device_code returned by auth_login
|
|
158
|
+
endpoint: API endpoint - must match the one used in auth_login
|
|
159
|
+
shop_id: Optional shop UUID to select (required if user has multiple shops)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
JSON with login result - success with credentials or pending/error status.
|
|
163
|
+
"""
|
|
164
|
+
args = ["auth", "login-complete", "--device-code", device_code, "--endpoint", endpoint, "--json"]
|
|
165
|
+
if shop_id:
|
|
166
|
+
args.extend(["--shop-id", shop_id])
|
|
167
|
+
result = _run_cli(args, timeout=30)
|
|
168
|
+
return json.dumps(result, indent=2)
|
|
169
|
+
|
|
170
|
+
|
|
109
171
|
@mcp.tool()
|
|
110
172
|
async def auth_status() -> str:
|
|
111
173
|
"""Check authentication status and current shop."""
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI and MCP server for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/AppliedLabsAI/applied-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/AppliedLabsAI/applied-cli
|
|
9
9
|
Keywords: applied-labs,ai-agents,support,mcp,claude
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
@@ -19,6 +19,10 @@ Requires-Dist: pyyaml>=6.0
|
|
|
19
19
|
Requires-Dist: typer>=0.16.0
|
|
20
20
|
Provides-Extra: mcp
|
|
21
21
|
Requires-Dist: mcp>=1.2.0; extra == "mcp"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
25
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
22
26
|
|
|
23
27
|
# Applied Labs CLI
|
|
24
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "applied-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "CLI and MCP server for Applied Labs AI support agents"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -26,14 +26,19 @@ dependencies = [
|
|
|
26
26
|
mcp = [
|
|
27
27
|
"mcp>=1.2.0",
|
|
28
28
|
]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
"build>=1.0",
|
|
32
|
+
"twine>=5.0",
|
|
33
|
+
]
|
|
29
34
|
|
|
30
35
|
[project.scripts]
|
|
31
36
|
applied-cli = "applied_cli.main:app"
|
|
32
37
|
applied-cli-mcp = "applied_cli.mcp_server:main"
|
|
33
38
|
|
|
34
39
|
[project.urls]
|
|
35
|
-
Homepage = "https://github.com/
|
|
36
|
-
Repository = "https://github.com/
|
|
40
|
+
Homepage = "https://github.com/AppliedLabsAI/applied-cli"
|
|
41
|
+
Repository = "https://github.com/AppliedLabsAI/applied-cli"
|
|
37
42
|
|
|
38
43
|
[tool.setuptools.packages.find]
|
|
39
44
|
include = ["applied_cli", "applied_cli.*"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|