applied-cli 0.5.74__tar.gz → 0.6.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.5.74 → applied_cli-0.6.0}/PKG-INFO +2 -1
- applied_cli-0.6.0/applied_cli/__init__.py +20 -0
- applied_cli-0.6.0/applied_cli/auth.py +111 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/cli.py +254 -6
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/client.py +82 -0
- applied_cli-0.6.0/applied_cli/mcp.py +222 -0
- applied_cli-0.6.0/applied_cli/toolkit.py +255 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/tools.py +4 -7
- applied_cli-0.6.0/applied_cli/v2/__init__.py +11 -0
- applied_cli-0.6.0/applied_cli/v2/agents.py +448 -0
- applied_cli-0.6.0/applied_cli/v2/articles.py +364 -0
- applied_cli-0.6.0/applied_cli/v2/catalog.py +278 -0
- applied_cli-0.6.0/applied_cli/v2/connectors.py +122 -0
- applied_cli-0.6.0/applied_cli/v2/content.py +337 -0
- applied_cli-0.6.0/applied_cli/v2/conversations.py +709 -0
- applied_cli-0.6.0/applied_cli/v2/domains.py +141 -0
- applied_cli-0.6.0/applied_cli/v2/flows.py +1889 -0
- applied_cli-0.6.0/applied_cli/v2/knowledge.py +609 -0
- applied_cli-0.6.0/applied_cli/v2/manifest.py +129 -0
- applied_cli-0.6.0/applied_cli/v2/products.py +350 -0
- applied_cli-0.6.0/applied_cli/v2/scenarios.py +948 -0
- applied_cli-0.6.0/applied_cli/v2/taxonomy.py +261 -0
- applied_cli-0.6.0/applied_cli/v2/tickets.py +292 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli.egg-info/PKG-INFO +2 -1
- applied_cli-0.6.0/applied_cli.egg-info/SOURCES.txt +60 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli.egg-info/requires.txt +1 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/pyproject.toml +2 -1
- applied_cli-0.6.0/tests/test_auth_context.py +51 -0
- applied_cli-0.6.0/tests/test_cli_v2.py +164 -0
- applied_cli-0.6.0/tests/test_client_v2.py +95 -0
- applied_cli-0.6.0/tests/test_toolkit_contract.py +230 -0
- applied_cli-0.6.0/tests/test_v2_agents.py +536 -0
- applied_cli-0.6.0/tests/test_v2_articles.py +387 -0
- applied_cli-0.6.0/tests/test_v2_catalog_and_mcp.py +327 -0
- applied_cli-0.6.0/tests/test_v2_connectors.py +100 -0
- applied_cli-0.6.0/tests/test_v2_content.py +253 -0
- applied_cli-0.6.0/tests/test_v2_conversations.py +426 -0
- applied_cli-0.6.0/tests/test_v2_flows.py +1740 -0
- applied_cli-0.6.0/tests/test_v2_knowledge.py +551 -0
- applied_cli-0.6.0/tests/test_v2_products.py +382 -0
- applied_cli-0.6.0/tests/test_v2_scenarios.py +876 -0
- applied_cli-0.6.0/tests/test_v2_taxonomy.py +284 -0
- applied_cli-0.6.0/tests/test_v2_tickets.py +328 -0
- applied_cli-0.5.74/applied_cli/__init__.py +0 -9
- applied_cli-0.5.74/applied_cli.egg-info/SOURCES.txt +0 -26
- {applied_cli-0.5.74 → applied_cli-0.6.0}/README.md +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/conversations.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/setup.cfg +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_cli.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_client.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.5.74 → applied_cli-0.6.0}/tests/test_knowledge_content_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: CLI and shared client library for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Requires-Python: >=3.11
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: pydantic>=2.0.0
|
|
18
19
|
Requires-Dist: typer>=0.9.0
|
|
19
20
|
Provides-Extra: dev
|
|
20
21
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Applied Labs CLI - shared client library for CLI and MCP server."""
|
|
2
|
+
|
|
3
|
+
from applied_cli import tools
|
|
4
|
+
from applied_cli.auth import AuthContext
|
|
5
|
+
from applied_cli.client import AppliedClient
|
|
6
|
+
from applied_cli.formatters import to_csv, to_json
|
|
7
|
+
from applied_cli.toolkit import ToolResult, ToolSpec
|
|
8
|
+
|
|
9
|
+
__version__ = "0.6.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AppliedClient",
|
|
13
|
+
"AuthContext",
|
|
14
|
+
"ToolResult",
|
|
15
|
+
"ToolSpec",
|
|
16
|
+
"tools",
|
|
17
|
+
"to_csv",
|
|
18
|
+
"to_json",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Shared authentication context for local CLI and MCP-injected sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from applied_cli.client import AppliedAPIError, AppliedClient
|
|
11
|
+
from applied_cli.credentials import load_credentials
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthStatus(BaseModel):
|
|
17
|
+
ok: bool
|
|
18
|
+
shop_id: str | None = None
|
|
19
|
+
base_url: str = DEFAULT_BASE_URL
|
|
20
|
+
message: str
|
|
21
|
+
status_code: int | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthContext(BaseModel):
|
|
25
|
+
token: str
|
|
26
|
+
shop_id: str | None = None
|
|
27
|
+
base_url: str = DEFAULT_BASE_URL
|
|
28
|
+
access_mode: str | None = None
|
|
29
|
+
|
|
30
|
+
def to_client(self) -> AppliedClient:
|
|
31
|
+
return AppliedClient(
|
|
32
|
+
token=self.token,
|
|
33
|
+
shop_id=self.shop_id,
|
|
34
|
+
base_url=self.base_url,
|
|
35
|
+
access_mode=self.access_mode,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def validate(self) -> AuthStatus:
|
|
39
|
+
client = self.to_client()
|
|
40
|
+
try:
|
|
41
|
+
await client.request(
|
|
42
|
+
"GET",
|
|
43
|
+
"/v1/agents/",
|
|
44
|
+
params={"limit": 1},
|
|
45
|
+
shop_id=self.shop_id,
|
|
46
|
+
)
|
|
47
|
+
except AppliedAPIError as exc:
|
|
48
|
+
return AuthStatus(
|
|
49
|
+
ok=False,
|
|
50
|
+
shop_id=self.shop_id,
|
|
51
|
+
base_url=self.base_url,
|
|
52
|
+
message=str(exc),
|
|
53
|
+
status_code=exc.status_code,
|
|
54
|
+
)
|
|
55
|
+
return AuthStatus(
|
|
56
|
+
ok=True,
|
|
57
|
+
shop_id=self.shop_id,
|
|
58
|
+
base_url=self.base_url,
|
|
59
|
+
message="Authenticated.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CredentialProvider(Protocol):
|
|
64
|
+
def load(self) -> AuthContext | None:
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EnvCredentialProvider:
|
|
69
|
+
"""Load credentials from environment variables for automation/MCP smoke tests."""
|
|
70
|
+
|
|
71
|
+
def load(self) -> AuthContext | None:
|
|
72
|
+
token = os.environ.get("APPLIED_TOKEN") or os.environ.get("APPLIED_API_TOKEN")
|
|
73
|
+
if not token:
|
|
74
|
+
return None
|
|
75
|
+
return AuthContext(
|
|
76
|
+
token=token,
|
|
77
|
+
shop_id=os.environ.get("APPLIED_SHOP_ID"),
|
|
78
|
+
base_url=os.environ.get("APPLIED_BASE_URL", DEFAULT_BASE_URL),
|
|
79
|
+
access_mode=os.environ.get("APPLIED_ASSISTANT_ACCESS_MODE"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class StoredCredentialProvider:
|
|
84
|
+
"""Load credentials written by `applied login`."""
|
|
85
|
+
|
|
86
|
+
def load(self) -> AuthContext | None:
|
|
87
|
+
credentials = load_credentials()
|
|
88
|
+
if not credentials or not credentials.get("api_token"):
|
|
89
|
+
return None
|
|
90
|
+
return AuthContext(
|
|
91
|
+
token=credentials["api_token"],
|
|
92
|
+
shop_id=credentials.get("shop_id"),
|
|
93
|
+
base_url=credentials.get("base_url", DEFAULT_BASE_URL),
|
|
94
|
+
access_mode=os.environ.get("APPLIED_ASSISTANT_ACCESS_MODE"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ChainedCredentialProvider:
|
|
99
|
+
def __init__(self, *providers: CredentialProvider):
|
|
100
|
+
self.providers = providers
|
|
101
|
+
|
|
102
|
+
def load(self) -> AuthContext | None:
|
|
103
|
+
for provider in self.providers:
|
|
104
|
+
context = provider.load()
|
|
105
|
+
if context:
|
|
106
|
+
return context
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def default_credential_provider() -> CredentialProvider:
|
|
111
|
+
return ChainedCredentialProvider(EnvCredentialProvider(), StoredCredentialProvider())
|
|
@@ -10,12 +10,21 @@ import typer
|
|
|
10
10
|
|
|
11
11
|
from applied_cli import __version__, tools
|
|
12
12
|
from applied_cli.agent_scoped_flows import run_agent_scoped_flow_command
|
|
13
|
+
from applied_cli.auth import AuthContext, default_credential_provider
|
|
13
14
|
from applied_cli.client import AppliedClient
|
|
14
15
|
from applied_cli.credentials import (
|
|
15
16
|
clear_credentials,
|
|
16
17
|
load_credentials,
|
|
17
18
|
save_credentials,
|
|
18
19
|
)
|
|
20
|
+
from applied_cli.toolkit import render_tool_result
|
|
21
|
+
from applied_cli.v2.catalog import (
|
|
22
|
+
execute_tool as execute_v2_tool,
|
|
23
|
+
)
|
|
24
|
+
from applied_cli.v2.catalog import (
|
|
25
|
+
get_tool_catalog,
|
|
26
|
+
)
|
|
27
|
+
from applied_cli.v2.manifest import build_doctor_report, build_tool_manifest
|
|
19
28
|
|
|
20
29
|
app = typer.Typer(
|
|
21
30
|
name="applied",
|
|
@@ -24,9 +33,19 @@ app = typer.Typer(
|
|
|
24
33
|
)
|
|
25
34
|
content_app = typer.Typer(help="Inspect synced content items.")
|
|
26
35
|
app.add_typer(content_app, name="content")
|
|
36
|
+
v2_app = typer.Typer(help="Run v2 typed Applied tools.")
|
|
37
|
+
v2_tools_app = typer.Typer(help="Inspect v2 tool schemas.")
|
|
38
|
+
v2_conversations_app = typer.Typer(help="Conversation tools.")
|
|
39
|
+
v2_app.add_typer(v2_tools_app, name="tools")
|
|
40
|
+
v2_app.add_typer(v2_conversations_app, name="conversations")
|
|
41
|
+
app.add_typer(v2_app, name="v2")
|
|
27
42
|
|
|
28
43
|
DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
|
|
29
44
|
DEFAULT_CLIENT_URL = "https://appliedlabs.ai"
|
|
45
|
+
AGENT_DEPLOY_AGENT_IDS_ARGUMENT = typer.Argument(
|
|
46
|
+
...,
|
|
47
|
+
help="One or more Agent IDs to deploy",
|
|
48
|
+
)
|
|
30
49
|
|
|
31
50
|
|
|
32
51
|
def _version_callback(value: bool) -> None:
|
|
@@ -144,6 +163,47 @@ def _build_conversation_filters(
|
|
|
144
163
|
return filters or None
|
|
145
164
|
|
|
146
165
|
|
|
166
|
+
def _v2_auth_context() -> AuthContext | None:
|
|
167
|
+
return default_credential_provider().load()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_v2_input(value: str | None) -> dict:
|
|
171
|
+
if not value:
|
|
172
|
+
return {}
|
|
173
|
+
parsed = _parse_json_option(value, option_name="--input")
|
|
174
|
+
return parsed or {}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _v2_tool_metadata(include_debug: bool = False) -> list[dict]:
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
"name": spec.name,
|
|
181
|
+
"namespace": spec.namespace,
|
|
182
|
+
"description": spec.description,
|
|
183
|
+
"read_write_mode": spec.read_write_mode,
|
|
184
|
+
"tags": spec.tags,
|
|
185
|
+
}
|
|
186
|
+
for spec in get_tool_catalog(include_debug=include_debug)
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _v2_tool_detail(tool_name: str, include_debug: bool = False) -> dict:
|
|
191
|
+
catalog = get_tool_catalog(include_debug=include_debug)
|
|
192
|
+
spec = catalog.get(tool_name)
|
|
193
|
+
if spec is None:
|
|
194
|
+
typer.echo(f"Unknown v2 tool: {tool_name}", err=True)
|
|
195
|
+
raise typer.Exit(1)
|
|
196
|
+
return {
|
|
197
|
+
"name": spec.name,
|
|
198
|
+
"namespace": spec.namespace,
|
|
199
|
+
"description": spec.description,
|
|
200
|
+
"read_write_mode": spec.read_write_mode,
|
|
201
|
+
"tags": spec.tags,
|
|
202
|
+
"examples": spec.examples,
|
|
203
|
+
"input_schema": spec.input_schema(),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
147
207
|
# -----------------------------------------------------------------------------
|
|
148
208
|
# Auth commands
|
|
149
209
|
# -----------------------------------------------------------------------------
|
|
@@ -230,7 +290,13 @@ def logout() -> None:
|
|
|
230
290
|
|
|
231
291
|
|
|
232
292
|
@app.command()
|
|
233
|
-
def whoami(
|
|
293
|
+
def whoami(
|
|
294
|
+
verify: bool = typer.Option(
|
|
295
|
+
True,
|
|
296
|
+
"--verify/--no-verify",
|
|
297
|
+
help="Verify the token against the Applied API.",
|
|
298
|
+
),
|
|
299
|
+
) -> None:
|
|
234
300
|
"""Show current authentication status."""
|
|
235
301
|
creds = load_credentials()
|
|
236
302
|
if not creds or not creds.get("api_token"):
|
|
@@ -243,6 +309,189 @@ def whoami() -> None:
|
|
|
243
309
|
typer.echo(f"Base URL: {creds.get('base_url', DEFAULT_BASE_URL)}")
|
|
244
310
|
if creds.get("shop_id"):
|
|
245
311
|
typer.echo(f"Shop ID: {creds['shop_id']}")
|
|
312
|
+
if verify:
|
|
313
|
+
context = AuthContext(
|
|
314
|
+
token=creds["api_token"],
|
|
315
|
+
shop_id=creds.get("shop_id"),
|
|
316
|
+
base_url=creds.get("base_url", DEFAULT_BASE_URL),
|
|
317
|
+
)
|
|
318
|
+
status = asyncio.run(context.validate())
|
|
319
|
+
typer.echo(f"Verified: {'yes' if status.ok else 'no'}")
|
|
320
|
+
if not status.ok:
|
|
321
|
+
typer.echo(status.message, err=True)
|
|
322
|
+
raise typer.Exit(1)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@v2_conversations_app.command("search")
|
|
326
|
+
def v2_conversations_search(
|
|
327
|
+
search: str | None = typer.Option(None, "--search", help="Full-text search query"),
|
|
328
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Max conversations"),
|
|
329
|
+
json_output: bool = typer.Option(
|
|
330
|
+
False,
|
|
331
|
+
"--json",
|
|
332
|
+
help="Render the structured tool result as JSON.",
|
|
333
|
+
),
|
|
334
|
+
) -> None:
|
|
335
|
+
"""Search conversations through the v2 typed tool catalog."""
|
|
336
|
+
|
|
337
|
+
async def _run():
|
|
338
|
+
return await execute_v2_tool(
|
|
339
|
+
"conversations_search",
|
|
340
|
+
{"search": search, "limit": limit},
|
|
341
|
+
auth_context=_v2_auth_context(),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
result = asyncio.run(_run())
|
|
345
|
+
typer.echo(render_tool_result(result, "json" if json_output else "text"))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@v2_tools_app.command("list")
|
|
349
|
+
def v2_tools_list(
|
|
350
|
+
include_debug: bool = typer.Option(
|
|
351
|
+
False,
|
|
352
|
+
"--include-debug",
|
|
353
|
+
help="Include gated debug tools.",
|
|
354
|
+
),
|
|
355
|
+
json_output: bool = typer.Option(
|
|
356
|
+
False,
|
|
357
|
+
"--json",
|
|
358
|
+
help="Render tool metadata as JSON.",
|
|
359
|
+
),
|
|
360
|
+
) -> None:
|
|
361
|
+
"""List registered v2 Applied tools."""
|
|
362
|
+
payload = {"tools": _v2_tool_metadata(include_debug=include_debug)}
|
|
363
|
+
if json_output:
|
|
364
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
for row in payload["tools"]:
|
|
368
|
+
first_line = row["description"].splitlines()[0]
|
|
369
|
+
typer.echo(
|
|
370
|
+
f"{row['name']}\t{row['read_write_mode']}\t{row['namespace']}\t{first_line}"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@v2_tools_app.command("describe")
|
|
375
|
+
def v2_tools_describe(
|
|
376
|
+
tool_name: str = typer.Argument(..., help="v2 tool name"),
|
|
377
|
+
include_debug: bool = typer.Option(
|
|
378
|
+
False,
|
|
379
|
+
"--include-debug",
|
|
380
|
+
help="Include gated debug tools.",
|
|
381
|
+
),
|
|
382
|
+
json_output: bool = typer.Option(
|
|
383
|
+
False,
|
|
384
|
+
"--json",
|
|
385
|
+
help="Render schema as JSON.",
|
|
386
|
+
),
|
|
387
|
+
) -> None:
|
|
388
|
+
"""Describe one v2 Applied tool and its input schema."""
|
|
389
|
+
payload = _v2_tool_detail(tool_name, include_debug=include_debug)
|
|
390
|
+
if json_output:
|
|
391
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
typer.echo(f"{payload['name']} ({payload['read_write_mode']})")
|
|
395
|
+
typer.echo(payload["description"])
|
|
396
|
+
typer.echo(json.dumps(payload["input_schema"], indent=2, default=str))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@v2_tools_app.command("manifest")
|
|
400
|
+
def v2_tools_manifest(
|
|
401
|
+
include_debug: bool = typer.Option(
|
|
402
|
+
False,
|
|
403
|
+
"--include-debug",
|
|
404
|
+
help="Include gated debug tools.",
|
|
405
|
+
),
|
|
406
|
+
json_output: bool = typer.Option(
|
|
407
|
+
False,
|
|
408
|
+
"--json",
|
|
409
|
+
help="Render the complete manifest as JSON.",
|
|
410
|
+
),
|
|
411
|
+
) -> None:
|
|
412
|
+
"""Emit the complete v2 tool manifest for agents and MCP packaging."""
|
|
413
|
+
payload = build_tool_manifest(include_debug=include_debug)
|
|
414
|
+
if json_output:
|
|
415
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
typer.echo(
|
|
419
|
+
f"Applied CLI v2 manifest: {payload['tool_count']} tools "
|
|
420
|
+
f"across {len(payload['namespace_counts'])} namespaces"
|
|
421
|
+
)
|
|
422
|
+
for name, count in payload["namespace_counts"].items():
|
|
423
|
+
typer.echo(f"{name}\t{count}")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@v2_app.command("doctor")
|
|
427
|
+
def v2_doctor(
|
|
428
|
+
include_debug: bool = typer.Option(
|
|
429
|
+
False,
|
|
430
|
+
"--include-debug",
|
|
431
|
+
help="Include gated debug tools in the catalog check.",
|
|
432
|
+
),
|
|
433
|
+
validate_auth: bool = typer.Option(
|
|
434
|
+
False,
|
|
435
|
+
"--validate-auth",
|
|
436
|
+
help="Verify configured credentials against the Applied API.",
|
|
437
|
+
),
|
|
438
|
+
json_output: bool = typer.Option(
|
|
439
|
+
False,
|
|
440
|
+
"--json",
|
|
441
|
+
help="Render diagnostics as JSON.",
|
|
442
|
+
),
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Check v2 catalog, MCP registration, and optional auth readiness."""
|
|
445
|
+
|
|
446
|
+
async def _run():
|
|
447
|
+
return await build_doctor_report(
|
|
448
|
+
include_debug=include_debug,
|
|
449
|
+
validate_auth=validate_auth,
|
|
450
|
+
credential_provider=default_credential_provider(),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
payload = asyncio.run(_run())
|
|
454
|
+
if json_output:
|
|
455
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
auth = payload["auth"]
|
|
459
|
+
typer.echo(f"Applied CLI {payload['version']}")
|
|
460
|
+
typer.echo(f"Catalog tools: {payload['catalog']['tool_count']}")
|
|
461
|
+
typer.echo(f"MCP registrar: {payload['mcp']['registrar']}")
|
|
462
|
+
typer.echo(f"Auth configured: {'yes' if auth['configured'] else 'no'}")
|
|
463
|
+
if validate_auth:
|
|
464
|
+
typer.echo(f"Auth valid: {'yes' if auth['ok'] else 'no'}")
|
|
465
|
+
if auth["message"]:
|
|
466
|
+
typer.echo(auth["message"])
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@v2_app.command("run")
|
|
470
|
+
def v2_run(
|
|
471
|
+
tool_name: str = typer.Argument(..., help="v2 tool name"),
|
|
472
|
+
input_json: str | None = typer.Option(
|
|
473
|
+
None,
|
|
474
|
+
"--input",
|
|
475
|
+
"-i",
|
|
476
|
+
help="JSON object input for the tool.",
|
|
477
|
+
),
|
|
478
|
+
json_output: bool = typer.Option(
|
|
479
|
+
False,
|
|
480
|
+
"--json",
|
|
481
|
+
help="Render the structured tool result as JSON.",
|
|
482
|
+
),
|
|
483
|
+
) -> None:
|
|
484
|
+
"""Run any v2 Applied tool by name with JSON input."""
|
|
485
|
+
|
|
486
|
+
async def _run():
|
|
487
|
+
return await execute_v2_tool(
|
|
488
|
+
tool_name,
|
|
489
|
+
_parse_v2_input(input_json),
|
|
490
|
+
auth_context=_v2_auth_context(),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
result = asyncio.run(_run())
|
|
494
|
+
typer.echo(render_tool_result(result, "json" if json_output else "text"))
|
|
246
495
|
|
|
247
496
|
|
|
248
497
|
# -----------------------------------------------------------------------------
|
|
@@ -2475,7 +2724,7 @@ def product_update(
|
|
|
2475
2724
|
|
|
2476
2725
|
@app.command("agent-deploy")
|
|
2477
2726
|
def agent_deploy(
|
|
2478
|
-
agent_ids: list[str] =
|
|
2727
|
+
agent_ids: list[str] = AGENT_DEPLOY_AGENT_IDS_ARGUMENT,
|
|
2479
2728
|
description: str = typer.Option("", "--description", "-d", help="Optional revision description"),
|
|
2480
2729
|
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2481
2730
|
) -> None:
|
|
@@ -2580,7 +2829,6 @@ def agent_revision_diff(
|
|
|
2580
2829
|
Example:
|
|
2581
2830
|
applied agent-revision-diff <agent_id> 54 55
|
|
2582
2831
|
"""
|
|
2583
|
-
import difflib
|
|
2584
2832
|
|
|
2585
2833
|
client = get_client(shop_id=shop_id)
|
|
2586
2834
|
|
|
@@ -2661,7 +2909,7 @@ def agent_revision_diff(
|
|
|
2661
2909
|
for field in ("model", "escalation_mode", "use_guardrails", "auto_reply", "response_delay_in_seconds"):
|
|
2662
2910
|
typer.echo(f" {field}: {agent.get(field)}")
|
|
2663
2911
|
|
|
2664
|
-
typer.echo(
|
|
2912
|
+
typer.echo("\n--- PROMPT (guardrail, current) ---")
|
|
2665
2913
|
guardrail = agent.get("guardrail") or ""
|
|
2666
2914
|
typer.echo(f" Length: {len(guardrail)} chars")
|
|
2667
2915
|
typer.echo(f" First 300 chars:\n{guardrail[:300]}")
|
|
@@ -2673,8 +2921,8 @@ def agent_revision_diff(
|
|
|
2673
2921
|
for f in active[:20]:
|
|
2674
2922
|
typer.echo(f" [{f.get('trigger','?')}] {f.get('name')} ({f.get('status')})")
|
|
2675
2923
|
|
|
2676
|
-
typer.echo(
|
|
2677
|
-
typer.echo(
|
|
2924
|
+
typer.echo("\nNote: Full prompt diff requires snapshot storage per revision (not yet available in API).")
|
|
2925
|
+
typer.echo("To investigate: compare guardrail text above against the known-good version,")
|
|
2678
2926
|
typer.echo(f"and check if any flows were added/removed between {rev_a['created_at'][:10]} and {rev_b['created_at'][:10]}.")
|
|
2679
2927
|
|
|
2680
2928
|
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import uuid
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
@@ -531,6 +532,25 @@ class AppliedClient:
|
|
|
531
532
|
# Do not let proposal logging mask the access-mode decision.
|
|
532
533
|
return
|
|
533
534
|
|
|
535
|
+
async def record_tool_access_mode_decision(
|
|
536
|
+
self,
|
|
537
|
+
*,
|
|
538
|
+
tool_name: str,
|
|
539
|
+
raw_input: dict[str, Any],
|
|
540
|
+
read_write_mode: str,
|
|
541
|
+
status: str,
|
|
542
|
+
) -> None:
|
|
543
|
+
await self._record_access_mode_change(
|
|
544
|
+
method="POST",
|
|
545
|
+
path=f"/mcp/tools/{tool_name}/",
|
|
546
|
+
body={
|
|
547
|
+
"tool_name": tool_name,
|
|
548
|
+
"input": raw_input,
|
|
549
|
+
"read_write_mode": read_write_mode,
|
|
550
|
+
},
|
|
551
|
+
status=status,
|
|
552
|
+
)
|
|
553
|
+
|
|
534
554
|
async def _fetch_current_object(
|
|
535
555
|
self,
|
|
536
556
|
*,
|
|
@@ -707,6 +727,68 @@ class AppliedClient:
|
|
|
707
727
|
)
|
|
708
728
|
return result
|
|
709
729
|
|
|
730
|
+
async def request(
|
|
731
|
+
self,
|
|
732
|
+
method: str,
|
|
733
|
+
path: str,
|
|
734
|
+
params: dict[str, Any] | None = None,
|
|
735
|
+
body: dict[str, Any] | None = None,
|
|
736
|
+
shop_id: str | None = None,
|
|
737
|
+
) -> Any:
|
|
738
|
+
"""Public authenticated request API for catalog tools and integrations."""
|
|
739
|
+
return await self._request(
|
|
740
|
+
method,
|
|
741
|
+
path,
|
|
742
|
+
params=params,
|
|
743
|
+
body=body,
|
|
744
|
+
shop_id=shop_id,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
async def iter_pages(
|
|
748
|
+
self,
|
|
749
|
+
path: str,
|
|
750
|
+
*,
|
|
751
|
+
params: dict[str, Any] | None = None,
|
|
752
|
+
page_style: str = "page",
|
|
753
|
+
shop_id: str | None = None,
|
|
754
|
+
max_pages: int | None = None,
|
|
755
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
756
|
+
"""Yield rows from page- or offset-paginated list endpoints."""
|
|
757
|
+
base_params = dict(params or {})
|
|
758
|
+
limit = int(base_params.get("limit") or 100)
|
|
759
|
+
page = int(base_params.get("page") or 1)
|
|
760
|
+
offset = int(base_params.get("offset") or 0)
|
|
761
|
+
pages_seen = 0
|
|
762
|
+
|
|
763
|
+
while True:
|
|
764
|
+
request_params = dict(base_params)
|
|
765
|
+
if page_style == "offset":
|
|
766
|
+
request_params["offset"] = offset
|
|
767
|
+
elif page_style == "page":
|
|
768
|
+
request_params["page"] = page
|
|
769
|
+
else:
|
|
770
|
+
raise ValueError("page_style must be 'page' or 'offset'")
|
|
771
|
+
|
|
772
|
+
data = await self.request(
|
|
773
|
+
"GET",
|
|
774
|
+
path,
|
|
775
|
+
params=request_params,
|
|
776
|
+
shop_id=shop_id,
|
|
777
|
+
)
|
|
778
|
+
rows = self._normalize_response(data)
|
|
779
|
+
for row in rows:
|
|
780
|
+
yield row
|
|
781
|
+
|
|
782
|
+
pages_seen += 1
|
|
783
|
+
if max_pages is not None and pages_seen >= max_pages:
|
|
784
|
+
break
|
|
785
|
+
if not isinstance(data, dict) or not data.get("next"):
|
|
786
|
+
break
|
|
787
|
+
if page_style == "offset":
|
|
788
|
+
offset += limit
|
|
789
|
+
else:
|
|
790
|
+
page += 1
|
|
791
|
+
|
|
710
792
|
async def _upload_request(
|
|
711
793
|
self,
|
|
712
794
|
method: str,
|