applied-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.
- applied_cli/__init__.py +2 -0
- applied_cli/auth_store.py +263 -0
- applied_cli/commands/__init__.py +2 -0
- applied_cli/commands/_hints.py +11 -0
- applied_cli/commands/_normalize.py +79 -0
- applied_cli/commands/_parsers.py +58 -0
- applied_cli/commands/_ui.py +33 -0
- applied_cli/commands/agent.py +1231 -0
- applied_cli/commands/auth.py +739 -0
- applied_cli/commands/chat.py +379 -0
- applied_cli/commands/coverage.py +348 -0
- applied_cli/commands/discover.py +1006 -0
- applied_cli/commands/fix.py +1204 -0
- applied_cli/commands/insights.py +614 -0
- applied_cli/commands/intents.py +447 -0
- applied_cli/commands/rate.py +508 -0
- applied_cli/commands/responses.py +604 -0
- applied_cli/commands/shop.py +1757 -0
- applied_cli/commands/simulate.py +330 -0
- applied_cli/commands/spec.py +238 -0
- applied_cli/config.py +50 -0
- applied_cli/error_reporting.py +38 -0
- applied_cli/http.py +1614 -0
- applied_cli/main.py +90 -0
- applied_cli/mcp_server.py +738 -0
- applied_cli/presets/demo.yaml +170 -0
- applied_cli/runtime.py +53 -0
- applied_cli/shop_spec.py +398 -0
- applied_cli/spec_workflow.py +432 -0
- applied_cli-0.1.0.dist-info/METADATA +176 -0
- applied_cli-0.1.0.dist-info/RECORD +34 -0
- applied_cli-0.1.0.dist-info/WHEEL +5 -0
- applied_cli-0.1.0.dist-info/entry_points.txt +3 -0
- applied_cli-0.1.0.dist-info/top_level.txt +1 -0
applied_cli/__init__.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from applied_cli.config import Credentials
|
|
6
|
+
|
|
7
|
+
SERVICE_NAME = "applied-cli"
|
|
8
|
+
ACCOUNT_NAME = "default"
|
|
9
|
+
STORE_VERSION = 2
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _fallback_path() -> Path:
|
|
13
|
+
return Path.home() / ".config" / "applied-cli" / "credentials.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _credentials_to_dict(credentials: Credentials) -> dict[str, str]:
|
|
17
|
+
return {
|
|
18
|
+
"base_url": credentials.base_url,
|
|
19
|
+
"shop_id": credentials.shop_id,
|
|
20
|
+
"api_token": credentials.api_token,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _credentials_from_dict(data: dict[str, object]) -> Optional[Credentials]:
|
|
25
|
+
base_url = str(data.get("base_url", "")).strip()
|
|
26
|
+
shop_id = str(data.get("shop_id", "")).strip()
|
|
27
|
+
api_token = str(data.get("api_token", "")).strip()
|
|
28
|
+
if not base_url or not shop_id or not api_token:
|
|
29
|
+
return None
|
|
30
|
+
return Credentials(base_url=base_url, shop_id=shop_id, api_token=api_token)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _normalize_store(raw: object) -> dict[str, object]:
|
|
34
|
+
if not isinstance(raw, dict):
|
|
35
|
+
return {"version": STORE_VERSION, "active_profile": None, "profiles": {}}
|
|
36
|
+
|
|
37
|
+
# Legacy single-profile shape:
|
|
38
|
+
# {"base_url": "...", "shop_id": "...", "api_token": "..."}
|
|
39
|
+
legacy = _credentials_from_dict(raw)
|
|
40
|
+
if legacy:
|
|
41
|
+
return {
|
|
42
|
+
"version": STORE_VERSION,
|
|
43
|
+
"active_profile": "default",
|
|
44
|
+
"profiles": {"default": _credentials_to_dict(legacy)},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
profiles_raw = raw.get("profiles")
|
|
48
|
+
profiles: dict[str, dict[str, str]] = {}
|
|
49
|
+
if isinstance(profiles_raw, dict):
|
|
50
|
+
for profile_name, payload in profiles_raw.items():
|
|
51
|
+
if not isinstance(profile_name, str) or not profile_name.strip():
|
|
52
|
+
continue
|
|
53
|
+
if not isinstance(payload, dict):
|
|
54
|
+
continue
|
|
55
|
+
creds = _credentials_from_dict(payload)
|
|
56
|
+
if creds is None:
|
|
57
|
+
continue
|
|
58
|
+
profiles[profile_name.strip()] = _credentials_to_dict(creds)
|
|
59
|
+
|
|
60
|
+
active_profile_raw = raw.get("active_profile")
|
|
61
|
+
active_profile = (
|
|
62
|
+
active_profile_raw.strip()
|
|
63
|
+
if isinstance(active_profile_raw, str) and active_profile_raw.strip()
|
|
64
|
+
else None
|
|
65
|
+
)
|
|
66
|
+
if active_profile and active_profile not in profiles:
|
|
67
|
+
active_profile = None
|
|
68
|
+
if not active_profile and profiles:
|
|
69
|
+
active_profile = sorted(profiles.keys())[0]
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"version": STORE_VERSION,
|
|
73
|
+
"active_profile": active_profile,
|
|
74
|
+
"profiles": profiles,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _serialize_store(store: dict[str, object]) -> str:
|
|
79
|
+
return json.dumps(
|
|
80
|
+
{
|
|
81
|
+
"version": STORE_VERSION,
|
|
82
|
+
"active_profile": store.get("active_profile"),
|
|
83
|
+
"profiles": store.get("profiles", {}),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_store_from_keyring() -> Optional[dict[str, object]]:
|
|
89
|
+
try:
|
|
90
|
+
import keyring
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
raw = keyring.get_password(SERVICE_NAME, ACCOUNT_NAME)
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if not raw:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
return _normalize_store(json.loads(raw))
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _save_store_to_keyring(store: dict[str, object]) -> bool:
|
|
109
|
+
try:
|
|
110
|
+
import keyring
|
|
111
|
+
except Exception:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
keyring.set_password(
|
|
116
|
+
SERVICE_NAME,
|
|
117
|
+
ACCOUNT_NAME,
|
|
118
|
+
_serialize_store(store),
|
|
119
|
+
)
|
|
120
|
+
return True
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _clear_keyring() -> None:
|
|
126
|
+
try:
|
|
127
|
+
import keyring
|
|
128
|
+
|
|
129
|
+
keyring.delete_password(SERVICE_NAME, ACCOUNT_NAME)
|
|
130
|
+
except Exception:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _load_store_from_file() -> Optional[dict[str, object]]:
|
|
135
|
+
path = _fallback_path()
|
|
136
|
+
if not path.exists():
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
return _normalize_store(json.loads(path.read_text()))
|
|
141
|
+
except Exception:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _save_store_to_file(store: dict[str, object]) -> None:
|
|
146
|
+
path = _fallback_path()
|
|
147
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
path.write_text(_serialize_store(store))
|
|
149
|
+
path.chmod(0o600)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _clear_file() -> None:
|
|
153
|
+
path = _fallback_path()
|
|
154
|
+
if path.exists():
|
|
155
|
+
path.unlink()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _load_store() -> dict[str, object]:
|
|
159
|
+
store = _load_store_from_keyring()
|
|
160
|
+
if store:
|
|
161
|
+
return store
|
|
162
|
+
store = _load_store_from_file()
|
|
163
|
+
if store:
|
|
164
|
+
return store
|
|
165
|
+
return {"version": STORE_VERSION, "active_profile": None, "profiles": {}}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _save_store(store: dict[str, object]) -> str:
|
|
169
|
+
if _save_store_to_keyring(store):
|
|
170
|
+
return "keyring"
|
|
171
|
+
|
|
172
|
+
_save_store_to_file(store)
|
|
173
|
+
return "file"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def save_credentials(
|
|
177
|
+
credentials: Credentials,
|
|
178
|
+
*,
|
|
179
|
+
profile: str = "default",
|
|
180
|
+
set_active: bool = True,
|
|
181
|
+
) -> str:
|
|
182
|
+
profile_name = profile.strip() or "default"
|
|
183
|
+
store = _load_store()
|
|
184
|
+
profiles = store.get("profiles")
|
|
185
|
+
if not isinstance(profiles, dict):
|
|
186
|
+
profiles = {}
|
|
187
|
+
profiles[profile_name] = _credentials_to_dict(credentials)
|
|
188
|
+
store["profiles"] = profiles
|
|
189
|
+
if set_active:
|
|
190
|
+
store["active_profile"] = profile_name
|
|
191
|
+
return _save_store(store)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def load_profiles() -> dict[str, Credentials]:
|
|
195
|
+
store = _load_store()
|
|
196
|
+
profiles = store.get("profiles")
|
|
197
|
+
if not isinstance(profiles, dict):
|
|
198
|
+
return {}
|
|
199
|
+
out: dict[str, Credentials] = {}
|
|
200
|
+
for profile_name, payload in profiles.items():
|
|
201
|
+
if not isinstance(profile_name, str) or not isinstance(payload, dict):
|
|
202
|
+
continue
|
|
203
|
+
creds = _credentials_from_dict(payload)
|
|
204
|
+
if creds is not None:
|
|
205
|
+
out[profile_name] = creds
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def list_profiles() -> list[str]:
|
|
210
|
+
return sorted(load_profiles().keys())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_active_profile_name() -> Optional[str]:
|
|
214
|
+
store = _load_store()
|
|
215
|
+
active = store.get("active_profile")
|
|
216
|
+
return active if isinstance(active, str) and active.strip() else None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_active_profile(profile: str) -> bool:
|
|
220
|
+
profile_name = profile.strip()
|
|
221
|
+
if not profile_name:
|
|
222
|
+
return False
|
|
223
|
+
store = _load_store()
|
|
224
|
+
profiles = store.get("profiles")
|
|
225
|
+
if not isinstance(profiles, dict) or profile_name not in profiles:
|
|
226
|
+
return False
|
|
227
|
+
store["active_profile"] = profile_name
|
|
228
|
+
_save_store(store)
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def delete_profile(profile: str) -> bool:
|
|
233
|
+
profile_name = profile.strip()
|
|
234
|
+
if not profile_name:
|
|
235
|
+
return False
|
|
236
|
+
store = _load_store()
|
|
237
|
+
profiles = store.get("profiles")
|
|
238
|
+
if not isinstance(profiles, dict) or profile_name not in profiles:
|
|
239
|
+
return False
|
|
240
|
+
del profiles[profile_name]
|
|
241
|
+
store["profiles"] = profiles
|
|
242
|
+
active = store.get("active_profile")
|
|
243
|
+
if active == profile_name:
|
|
244
|
+
store["active_profile"] = sorted(profiles.keys())[0] if profiles else None
|
|
245
|
+
_save_store(store)
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def load_credentials(*, profile: Optional[str] = None) -> Optional[Credentials]:
|
|
250
|
+
profiles = load_profiles()
|
|
251
|
+
if not profiles:
|
|
252
|
+
return None
|
|
253
|
+
if profile and profile in profiles:
|
|
254
|
+
return profiles[profile]
|
|
255
|
+
active = get_active_profile_name()
|
|
256
|
+
if active and active in profiles:
|
|
257
|
+
return profiles[active]
|
|
258
|
+
return profiles.get("default")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def clear_credentials() -> None:
|
|
262
|
+
_clear_keyring()
|
|
263
|
+
_clear_file()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from difflib import get_close_matches
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def suggest_value(raw: str, options: Iterable[str]) -> str | None:
|
|
6
|
+
value = raw.strip().lower()
|
|
7
|
+
normalized = [item.strip().lower() for item in options if item.strip()]
|
|
8
|
+
if not value or not normalized:
|
|
9
|
+
return None
|
|
10
|
+
matches = get_close_matches(value, normalized, n=1, cutoff=0.6)
|
|
11
|
+
return matches[0] if matches else None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from applied_cli.commands._hints import suggest_value
|
|
7
|
+
|
|
8
|
+
MODALITY_ALIASES = {
|
|
9
|
+
"all": "All",
|
|
10
|
+
"call": "Call",
|
|
11
|
+
"sms": "SMS",
|
|
12
|
+
"email": "Email",
|
|
13
|
+
"chat": "Chat",
|
|
14
|
+
"internal": "Internal",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
AGENT_TYPE_ALIASES = {
|
|
18
|
+
"generic": "Generic",
|
|
19
|
+
"customer_support": "Customer Support",
|
|
20
|
+
"customer support": "Customer Support",
|
|
21
|
+
"conversion": "Conversion",
|
|
22
|
+
"sales": "Sales",
|
|
23
|
+
"marketing": "Marketing",
|
|
24
|
+
"engineering": "Engineering",
|
|
25
|
+
"product": "Product",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
RESPONSE_TYPE_ALIASES = {
|
|
29
|
+
"escalation": "escalate",
|
|
30
|
+
"escalate": "escalate",
|
|
31
|
+
"exact": "exact",
|
|
32
|
+
"qa": "qa",
|
|
33
|
+
"context": "context",
|
|
34
|
+
"greeting": "greeting",
|
|
35
|
+
"signature": "signature",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_question(text: str) -> str:
|
|
40
|
+
return re.sub(r"\s+", " ", text.strip().lower())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_response_type(
|
|
44
|
+
raw: str,
|
|
45
|
+
*,
|
|
46
|
+
field_label: str = "type",
|
|
47
|
+
include_suggestion: bool = False,
|
|
48
|
+
) -> str:
|
|
49
|
+
key = raw.strip().lower()
|
|
50
|
+
if key not in RESPONSE_TYPE_ALIASES:
|
|
51
|
+
suffix = ""
|
|
52
|
+
if include_suggestion:
|
|
53
|
+
suggestion = suggest_value(key, RESPONSE_TYPE_ALIASES.keys())
|
|
54
|
+
if suggestion:
|
|
55
|
+
suffix = f" Did you mean '{suggestion}'?"
|
|
56
|
+
raise typer.BadParameter(
|
|
57
|
+
f"{field_label} must be one of: escalation, exact, qa, context, greeting, signature.{suffix}"
|
|
58
|
+
)
|
|
59
|
+
return RESPONSE_TYPE_ALIASES[key]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize_modality(raw: Optional[str]) -> Optional[str]:
|
|
63
|
+
if raw is None:
|
|
64
|
+
return None
|
|
65
|
+
key = raw.strip().lower()
|
|
66
|
+
if key not in MODALITY_ALIASES:
|
|
67
|
+
raise typer.BadParameter("modality must be one of: all, call, sms, email, chat, internal")
|
|
68
|
+
return MODALITY_ALIASES[key]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def normalize_agent_type(raw: Optional[str]) -> Optional[str]:
|
|
72
|
+
if raw is None:
|
|
73
|
+
return None
|
|
74
|
+
key = raw.strip().lower()
|
|
75
|
+
if key not in AGENT_TYPE_ALIASES:
|
|
76
|
+
raise typer.BadParameter(
|
|
77
|
+
"type must be one of: generic, customer_support, conversion, sales, marketing, engineering, product"
|
|
78
|
+
)
|
|
79
|
+
return AGENT_TYPE_ALIASES[key]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_uuid(value: str, *, field_name: str) -> None:
|
|
9
|
+
try:
|
|
10
|
+
uuid.UUID(value)
|
|
11
|
+
except ValueError as exc:
|
|
12
|
+
raise typer.BadParameter(f"{field_name} must be a valid UUID.") from exc
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_optional_bool(raw: Optional[str], *, field_name: str) -> Optional[bool]:
|
|
16
|
+
if raw is None:
|
|
17
|
+
return None
|
|
18
|
+
normalized = raw.strip().lower()
|
|
19
|
+
if normalized in {"1", "true", "t", "yes", "y"}:
|
|
20
|
+
return True
|
|
21
|
+
if normalized in {"0", "false", "f", "no", "n"}:
|
|
22
|
+
return False
|
|
23
|
+
raise typer.BadParameter(f"{field_name} expected one of: true/false/1/0/yes/no")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_json_dict(raw: Optional[str], *, field_name: str) -> Optional[dict[str, Any]]:
|
|
27
|
+
if raw is None:
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
parsed = json.loads(raw)
|
|
31
|
+
except json.JSONDecodeError as exc:
|
|
32
|
+
raise typer.BadParameter(f"{field_name} must be valid JSON.") from exc
|
|
33
|
+
if not isinstance(parsed, dict):
|
|
34
|
+
raise typer.BadParameter(f"{field_name} must decode to a JSON object.")
|
|
35
|
+
return parsed
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_json_array(raw: Optional[str], *, field_name: str) -> Optional[list[Any]]:
|
|
39
|
+
if raw is None:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
parsed = json.loads(raw)
|
|
43
|
+
except json.JSONDecodeError as exc:
|
|
44
|
+
raise typer.BadParameter(f"{field_name} must be valid JSON.") from exc
|
|
45
|
+
if not isinstance(parsed, list):
|
|
46
|
+
raise typer.BadParameter(f"{field_name} must decode to a JSON array.")
|
|
47
|
+
return parsed
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_csv_list(raw: Optional[str]) -> list[str]:
|
|
51
|
+
if not raw:
|
|
52
|
+
return []
|
|
53
|
+
return [token.strip() for token in raw.split(",") if token.strip()]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_uuid_list(values: list[str], *, field_name: str) -> None:
|
|
57
|
+
for value in values:
|
|
58
|
+
validate_uuid(value, field_name=field_name)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def show_target(fields: dict[str, Any]) -> None:
|
|
8
|
+
typer.echo("Target:")
|
|
9
|
+
for key, value in fields.items():
|
|
10
|
+
typer.echo(f"- {key}: {value}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def confirm_or_exit(*, yes: bool, prompt: str = "Continue?") -> None:
|
|
14
|
+
if not yes and not typer.confirm(prompt):
|
|
15
|
+
raise typer.Exit(code=1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def emit_dry_run(*, payload: dict[str, Any], output_json: bool) -> None:
|
|
19
|
+
typer.echo(json.dumps(payload, indent=2) if output_json else str(payload))
|
|
20
|
+
raise typer.Exit(code=0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def emit_success(
|
|
24
|
+
*,
|
|
25
|
+
output_json: bool,
|
|
26
|
+
payload: Any,
|
|
27
|
+
fields: dict[str, Any],
|
|
28
|
+
) -> None:
|
|
29
|
+
if output_json:
|
|
30
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
31
|
+
return
|
|
32
|
+
items = [f"{key}={value}" for key, value in fields.items()]
|
|
33
|
+
typer.echo("result=success | " + " | ".join(items))
|