applied-cli 0.5.74__tar.gz → 0.6.1__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.
Files changed (67) hide show
  1. {applied_cli-0.5.74 → applied_cli-0.6.1}/PKG-INFO +2 -1
  2. applied_cli-0.6.1/applied_cli/__init__.py +20 -0
  3. applied_cli-0.6.1/applied_cli/auth.py +111 -0
  4. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/cli.py +355 -6
  5. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/client.py +136 -0
  6. applied_cli-0.6.1/applied_cli/mcp.py +222 -0
  7. applied_cli-0.6.1/applied_cli/recovery.py +400 -0
  8. applied_cli-0.6.1/applied_cli/toolkit.py +255 -0
  9. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/tools.py +201 -7
  10. applied_cli-0.6.1/applied_cli/v2/__init__.py +11 -0
  11. applied_cli-0.6.1/applied_cli/v2/agents.py +448 -0
  12. applied_cli-0.6.1/applied_cli/v2/articles.py +364 -0
  13. applied_cli-0.6.1/applied_cli/v2/catalog.py +278 -0
  14. applied_cli-0.6.1/applied_cli/v2/connectors.py +122 -0
  15. applied_cli-0.6.1/applied_cli/v2/content.py +337 -0
  16. applied_cli-0.6.1/applied_cli/v2/conversations.py +709 -0
  17. applied_cli-0.6.1/applied_cli/v2/domains.py +142 -0
  18. applied_cli-0.6.1/applied_cli/v2/flows.py +1889 -0
  19. applied_cli-0.6.1/applied_cli/v2/knowledge.py +609 -0
  20. applied_cli-0.6.1/applied_cli/v2/manifest.py +129 -0
  21. applied_cli-0.6.1/applied_cli/v2/products.py +350 -0
  22. applied_cli-0.6.1/applied_cli/v2/scenarios.py +1012 -0
  23. applied_cli-0.6.1/applied_cli/v2/taxonomy.py +261 -0
  24. applied_cli-0.6.1/applied_cli/v2/tickets.py +292 -0
  25. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/PKG-INFO +2 -1
  26. applied_cli-0.6.1/applied_cli.egg-info/SOURCES.txt +63 -0
  27. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/requires.txt +1 -0
  28. {applied_cli-0.5.74 → applied_cli-0.6.1}/pyproject.toml +2 -1
  29. applied_cli-0.6.1/tests/test_auth_context.py +51 -0
  30. applied_cli-0.6.1/tests/test_benchmark_clone.py +242 -0
  31. applied_cli-0.6.1/tests/test_cli_v2.py +164 -0
  32. applied_cli-0.6.1/tests/test_client_v2.py +95 -0
  33. applied_cli-0.6.1/tests/test_recovery.py +351 -0
  34. applied_cli-0.6.1/tests/test_toolkit_contract.py +230 -0
  35. applied_cli-0.6.1/tests/test_v2_agents.py +536 -0
  36. applied_cli-0.6.1/tests/test_v2_articles.py +387 -0
  37. applied_cli-0.6.1/tests/test_v2_catalog_and_mcp.py +327 -0
  38. applied_cli-0.6.1/tests/test_v2_connectors.py +100 -0
  39. applied_cli-0.6.1/tests/test_v2_content.py +253 -0
  40. applied_cli-0.6.1/tests/test_v2_conversations.py +426 -0
  41. applied_cli-0.6.1/tests/test_v2_flows.py +1740 -0
  42. applied_cli-0.6.1/tests/test_v2_knowledge.py +551 -0
  43. applied_cli-0.6.1/tests/test_v2_products.py +382 -0
  44. applied_cli-0.6.1/tests/test_v2_scenarios.py +876 -0
  45. applied_cli-0.6.1/tests/test_v2_taxonomy.py +284 -0
  46. applied_cli-0.6.1/tests/test_v2_tickets.py +328 -0
  47. applied_cli-0.5.74/applied_cli/__init__.py +0 -9
  48. applied_cli-0.5.74/applied_cli.egg-info/SOURCES.txt +0 -26
  49. {applied_cli-0.5.74 → applied_cli-0.6.1}/README.md +0 -0
  50. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/agent_scoped_flows.py +0 -0
  51. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/conversation_lookup.py +0 -0
  52. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/conversations.py +0 -0
  53. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/credentials.py +0 -0
  54. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/flow_helpers.py +0 -0
  55. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli/formatters.py +0 -0
  56. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/dependency_links.txt +0 -0
  57. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/entry_points.txt +0 -0
  58. {applied_cli-0.5.74 → applied_cli-0.6.1}/applied_cli.egg-info/top_level.txt +0 -0
  59. {applied_cli-0.5.74 → applied_cli-0.6.1}/setup.cfg +0 -0
  60. {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_agent_scoped_flows.py +0 -0
  61. {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_audit_tools.py +0 -0
  62. {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_benchmark_scenario_tools.py +0 -0
  63. {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_cli.py +0 -0
  64. {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_client.py +0 -0
  65. {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_conversation_tools.py +0 -0
  66. {applied_cli-0.5.74 → applied_cli-0.6.1}/tests/test_flow_tools.py +0 -0
  67. {applied_cli-0.5.74 → applied_cli-0.6.1}/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.5.74
3
+ Version: 0.6.1
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,24 @@ 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
+ DEFAULT_RECOVERY_DIR_OPTION = typer.Option(
46
+ "/tmp/applied-labs-recovery-jun4-1915",
47
+ "--recovery-dir",
48
+ help="Directory containing exports/ and deleted_only/ recovery files",
49
+ )
50
+ AGENT_DEPLOY_AGENT_IDS_ARGUMENT = typer.Argument(
51
+ ...,
52
+ help="One or more Agent IDs to deploy",
53
+ )
30
54
 
31
55
 
32
56
  def _version_callback(value: bool) -> None:
@@ -144,6 +168,47 @@ def _build_conversation_filters(
144
168
  return filters or None
145
169
 
146
170
 
171
+ def _v2_auth_context() -> AuthContext | None:
172
+ return default_credential_provider().load()
173
+
174
+
175
+ def _parse_v2_input(value: str | None) -> dict:
176
+ if not value:
177
+ return {}
178
+ parsed = _parse_json_option(value, option_name="--input")
179
+ return parsed or {}
180
+
181
+
182
+ def _v2_tool_metadata(include_debug: bool = False) -> list[dict]:
183
+ return [
184
+ {
185
+ "name": spec.name,
186
+ "namespace": spec.namespace,
187
+ "description": spec.description,
188
+ "read_write_mode": spec.read_write_mode,
189
+ "tags": spec.tags,
190
+ }
191
+ for spec in get_tool_catalog(include_debug=include_debug)
192
+ ]
193
+
194
+
195
+ def _v2_tool_detail(tool_name: str, include_debug: bool = False) -> dict:
196
+ catalog = get_tool_catalog(include_debug=include_debug)
197
+ spec = catalog.get(tool_name)
198
+ if spec is None:
199
+ typer.echo(f"Unknown v2 tool: {tool_name}", err=True)
200
+ raise typer.Exit(1)
201
+ return {
202
+ "name": spec.name,
203
+ "namespace": spec.namespace,
204
+ "description": spec.description,
205
+ "read_write_mode": spec.read_write_mode,
206
+ "tags": spec.tags,
207
+ "examples": spec.examples,
208
+ "input_schema": spec.input_schema(),
209
+ }
210
+
211
+
147
212
  # -----------------------------------------------------------------------------
148
213
  # Auth commands
149
214
  # -----------------------------------------------------------------------------
@@ -230,7 +295,13 @@ def logout() -> None:
230
295
 
231
296
 
232
297
  @app.command()
233
- def whoami() -> None:
298
+ def whoami(
299
+ verify: bool = typer.Option(
300
+ True,
301
+ "--verify/--no-verify",
302
+ help="Verify the token against the Applied API.",
303
+ ),
304
+ ) -> None:
234
305
  """Show current authentication status."""
235
306
  creds = load_credentials()
236
307
  if not creds or not creds.get("api_token"):
@@ -243,6 +314,189 @@ def whoami() -> None:
243
314
  typer.echo(f"Base URL: {creds.get('base_url', DEFAULT_BASE_URL)}")
244
315
  if creds.get("shop_id"):
245
316
  typer.echo(f"Shop ID: {creds['shop_id']}")
317
+ if verify:
318
+ context = AuthContext(
319
+ token=creds["api_token"],
320
+ shop_id=creds.get("shop_id"),
321
+ base_url=creds.get("base_url", DEFAULT_BASE_URL),
322
+ )
323
+ status = asyncio.run(context.validate())
324
+ typer.echo(f"Verified: {'yes' if status.ok else 'no'}")
325
+ if not status.ok:
326
+ typer.echo(status.message, err=True)
327
+ raise typer.Exit(1)
328
+
329
+
330
+ @v2_conversations_app.command("search")
331
+ def v2_conversations_search(
332
+ search: str | None = typer.Option(None, "--search", help="Full-text search query"),
333
+ limit: int = typer.Option(10, "--limit", "-l", help="Max conversations"),
334
+ json_output: bool = typer.Option(
335
+ False,
336
+ "--json",
337
+ help="Render the structured tool result as JSON.",
338
+ ),
339
+ ) -> None:
340
+ """Search conversations through the v2 typed tool catalog."""
341
+
342
+ async def _run():
343
+ return await execute_v2_tool(
344
+ "conversations_search",
345
+ {"search": search, "limit": limit},
346
+ auth_context=_v2_auth_context(),
347
+ )
348
+
349
+ result = asyncio.run(_run())
350
+ typer.echo(render_tool_result(result, "json" if json_output else "text"))
351
+
352
+
353
+ @v2_tools_app.command("list")
354
+ def v2_tools_list(
355
+ include_debug: bool = typer.Option(
356
+ False,
357
+ "--include-debug",
358
+ help="Include gated debug tools.",
359
+ ),
360
+ json_output: bool = typer.Option(
361
+ False,
362
+ "--json",
363
+ help="Render tool metadata as JSON.",
364
+ ),
365
+ ) -> None:
366
+ """List registered v2 Applied tools."""
367
+ payload = {"tools": _v2_tool_metadata(include_debug=include_debug)}
368
+ if json_output:
369
+ typer.echo(json.dumps(payload, indent=2, default=str))
370
+ return
371
+
372
+ for row in payload["tools"]:
373
+ first_line = row["description"].splitlines()[0]
374
+ typer.echo(
375
+ f"{row['name']}\t{row['read_write_mode']}\t{row['namespace']}\t{first_line}"
376
+ )
377
+
378
+
379
+ @v2_tools_app.command("describe")
380
+ def v2_tools_describe(
381
+ tool_name: str = typer.Argument(..., help="v2 tool name"),
382
+ include_debug: bool = typer.Option(
383
+ False,
384
+ "--include-debug",
385
+ help="Include gated debug tools.",
386
+ ),
387
+ json_output: bool = typer.Option(
388
+ False,
389
+ "--json",
390
+ help="Render schema as JSON.",
391
+ ),
392
+ ) -> None:
393
+ """Describe one v2 Applied tool and its input schema."""
394
+ payload = _v2_tool_detail(tool_name, include_debug=include_debug)
395
+ if json_output:
396
+ typer.echo(json.dumps(payload, indent=2, default=str))
397
+ return
398
+
399
+ typer.echo(f"{payload['name']} ({payload['read_write_mode']})")
400
+ typer.echo(payload["description"])
401
+ typer.echo(json.dumps(payload["input_schema"], indent=2, default=str))
402
+
403
+
404
+ @v2_tools_app.command("manifest")
405
+ def v2_tools_manifest(
406
+ include_debug: bool = typer.Option(
407
+ False,
408
+ "--include-debug",
409
+ help="Include gated debug tools.",
410
+ ),
411
+ json_output: bool = typer.Option(
412
+ False,
413
+ "--json",
414
+ help="Render the complete manifest as JSON.",
415
+ ),
416
+ ) -> None:
417
+ """Emit the complete v2 tool manifest for agents and MCP packaging."""
418
+ payload = build_tool_manifest(include_debug=include_debug)
419
+ if json_output:
420
+ typer.echo(json.dumps(payload, indent=2, default=str))
421
+ return
422
+
423
+ typer.echo(
424
+ f"Applied CLI v2 manifest: {payload['tool_count']} tools "
425
+ f"across {len(payload['namespace_counts'])} namespaces"
426
+ )
427
+ for name, count in payload["namespace_counts"].items():
428
+ typer.echo(f"{name}\t{count}")
429
+
430
+
431
+ @v2_app.command("doctor")
432
+ def v2_doctor(
433
+ include_debug: bool = typer.Option(
434
+ False,
435
+ "--include-debug",
436
+ help="Include gated debug tools in the catalog check.",
437
+ ),
438
+ validate_auth: bool = typer.Option(
439
+ False,
440
+ "--validate-auth",
441
+ help="Verify configured credentials against the Applied API.",
442
+ ),
443
+ json_output: bool = typer.Option(
444
+ False,
445
+ "--json",
446
+ help="Render diagnostics as JSON.",
447
+ ),
448
+ ) -> None:
449
+ """Check v2 catalog, MCP registration, and optional auth readiness."""
450
+
451
+ async def _run():
452
+ return await build_doctor_report(
453
+ include_debug=include_debug,
454
+ validate_auth=validate_auth,
455
+ credential_provider=default_credential_provider(),
456
+ )
457
+
458
+ payload = asyncio.run(_run())
459
+ if json_output:
460
+ typer.echo(json.dumps(payload, indent=2, default=str))
461
+ return
462
+
463
+ auth = payload["auth"]
464
+ typer.echo(f"Applied CLI {payload['version']}")
465
+ typer.echo(f"Catalog tools: {payload['catalog']['tool_count']}")
466
+ typer.echo(f"MCP registrar: {payload['mcp']['registrar']}")
467
+ typer.echo(f"Auth configured: {'yes' if auth['configured'] else 'no'}")
468
+ if validate_auth:
469
+ typer.echo(f"Auth valid: {'yes' if auth['ok'] else 'no'}")
470
+ if auth["message"]:
471
+ typer.echo(auth["message"])
472
+
473
+
474
+ @v2_app.command("run")
475
+ def v2_run(
476
+ tool_name: str = typer.Argument(..., help="v2 tool name"),
477
+ input_json: str | None = typer.Option(
478
+ None,
479
+ "--input",
480
+ "-i",
481
+ help="JSON object input for the tool.",
482
+ ),
483
+ json_output: bool = typer.Option(
484
+ False,
485
+ "--json",
486
+ help="Render the structured tool result as JSON.",
487
+ ),
488
+ ) -> None:
489
+ """Run any v2 Applied tool by name with JSON input."""
490
+
491
+ async def _run():
492
+ return await execute_v2_tool(
493
+ tool_name,
494
+ _parse_v2_input(input_json),
495
+ auth_context=_v2_auth_context(),
496
+ )
497
+
498
+ result = asyncio.run(_run())
499
+ typer.echo(render_tool_result(result, "json" if json_output else "text"))
246
500
 
247
501
 
248
502
  # -----------------------------------------------------------------------------
@@ -1343,6 +1597,49 @@ def benchmark_create(
1343
1597
  typer.echo(result)
1344
1598
 
1345
1599
 
1600
+ @app.command("benchmark-clone")
1601
+ def benchmark_clone(
1602
+ source_benchmark_id: str = typer.Argument(
1603
+ ..., help="Benchmark to copy scenarios from"
1604
+ ),
1605
+ dest_benchmark_id: str = typer.Option(
1606
+ None, "--dest-benchmark-id", help="Existing destination benchmark UUID"
1607
+ ),
1608
+ dest_benchmark_name: str = typer.Option(
1609
+ None,
1610
+ "--dest-benchmark-name",
1611
+ help="Destination benchmark name (found or created under --target-agent-id)",
1612
+ ),
1613
+ target_agent_id: str = typer.Option(
1614
+ None,
1615
+ "--target-agent-id",
1616
+ help="Agent for the destination benchmark when created by name "
1617
+ "(e.g. port an email benchmark onto the chat agent)",
1618
+ ),
1619
+ apply: bool = typer.Option(
1620
+ False, "--apply", help="Write changes (default is a dry-run plan)"
1621
+ ),
1622
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1623
+ format: str = typer.Option(
1624
+ "text", "--format", "-f", help="Output format: text or json"
1625
+ ),
1626
+ ) -> None:
1627
+ """Copy all scenarios from one benchmark into another (e.g. email -> chat)."""
1628
+ client = get_client(shop_id=shop_id)
1629
+ result = asyncio.run(
1630
+ tools.benchmark_clone(
1631
+ client,
1632
+ source_benchmark_id=source_benchmark_id,
1633
+ dest_benchmark_id=dest_benchmark_id,
1634
+ dest_benchmark_name=dest_benchmark_name,
1635
+ target_agent_id=target_agent_id,
1636
+ dry_run=not apply,
1637
+ output_format=format,
1638
+ )
1639
+ )
1640
+ typer.echo(result)
1641
+
1642
+
1346
1643
  @app.command()
1347
1644
  def scenarios(
1348
1645
  benchmark_id: str = typer.Option(
@@ -1523,6 +1820,59 @@ def scenario_run_update_cmd(
1523
1820
  typer.echo(result)
1524
1821
 
1525
1822
 
1823
+ @app.command("scenario-recover-catalog")
1824
+ def scenario_recover_catalog(
1825
+ recovery_dir: Path = DEFAULT_RECOVERY_DIR_OPTION,
1826
+ apply: bool = typer.Option(
1827
+ False,
1828
+ "--apply",
1829
+ help="Create missing benchmarks/scenarios and restore rated runs",
1830
+ ),
1831
+ restore_ratings: bool = typer.Option(
1832
+ True,
1833
+ "--restore-ratings/--no-restore-ratings",
1834
+ help="Restore rated scenario runs linked to recovered scenarios",
1835
+ ),
1836
+ max_rated_runs: int | None = typer.Option(
1837
+ None,
1838
+ "--max-rated-runs",
1839
+ help="Cap rated run restore count for staged recovery",
1840
+ ),
1841
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1842
+ format: str = typer.Option(
1843
+ "json", "--format", "-f", help="Output format: json or text"
1844
+ ),
1845
+ ) -> None:
1846
+ """Recover deleted benchmark/scenario catalog rows from local PITR exports."""
1847
+ from applied_cli.recovery import recover_scenario_catalog
1848
+
1849
+ client = get_client(shop_id=shop_id)
1850
+ result = asyncio.run(
1851
+ recover_scenario_catalog(
1852
+ client,
1853
+ recovery_dir=recovery_dir,
1854
+ dry_run=not apply,
1855
+ restore_ratings=restore_ratings,
1856
+ max_rated_runs=max_rated_runs,
1857
+ )
1858
+ )
1859
+ if format == "text":
1860
+ typer.echo(
1861
+ "\n".join(
1862
+ [
1863
+ f"dry_run: {result['dry_run']}",
1864
+ f"shop_id: {result['shop_id']}",
1865
+ f"restore_point_utc: {result['restore_point_utc']}",
1866
+ f"benchmarks: {result['benchmarks']}",
1867
+ f"scenarios: {result['scenarios']}",
1868
+ f"scenario_runs: {result['scenario_runs']}",
1869
+ ]
1870
+ )
1871
+ )
1872
+ return
1873
+ typer.echo(json.dumps(result, indent=2, default=str))
1874
+
1875
+
1526
1876
  @app.command("scenario-bulk-run")
1527
1877
  def scenario_bulk_run(
1528
1878
  scenario_ids: str = typer.Option(
@@ -2475,7 +2825,7 @@ def product_update(
2475
2825
 
2476
2826
  @app.command("agent-deploy")
2477
2827
  def agent_deploy(
2478
- agent_ids: list[str] = typer.Argument(..., help="One or more Agent IDs to deploy"),
2828
+ agent_ids: list[str] = AGENT_DEPLOY_AGENT_IDS_ARGUMENT,
2479
2829
  description: str = typer.Option("", "--description", "-d", help="Optional revision description"),
2480
2830
  shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
2481
2831
  ) -> None:
@@ -2580,7 +2930,6 @@ def agent_revision_diff(
2580
2930
  Example:
2581
2931
  applied agent-revision-diff <agent_id> 54 55
2582
2932
  """
2583
- import difflib
2584
2933
 
2585
2934
  client = get_client(shop_id=shop_id)
2586
2935
 
@@ -2661,7 +3010,7 @@ def agent_revision_diff(
2661
3010
  for field in ("model", "escalation_mode", "use_guardrails", "auto_reply", "response_delay_in_seconds"):
2662
3011
  typer.echo(f" {field}: {agent.get(field)}")
2663
3012
 
2664
- typer.echo(f"\n--- PROMPT (guardrail, current) ---")
3013
+ typer.echo("\n--- PROMPT (guardrail, current) ---")
2665
3014
  guardrail = agent.get("guardrail") or ""
2666
3015
  typer.echo(f" Length: {len(guardrail)} chars")
2667
3016
  typer.echo(f" First 300 chars:\n{guardrail[:300]}")
@@ -2673,8 +3022,8 @@ def agent_revision_diff(
2673
3022
  for f in active[:20]:
2674
3023
  typer.echo(f" [{f.get('trigger','?')}] {f.get('name')} ({f.get('status')})")
2675
3024
 
2676
- typer.echo(f"\nNote: Full prompt diff requires snapshot storage per revision (not yet available in API).")
2677
- typer.echo(f"To investigate: compare guardrail text above against the known-good version,")
3025
+ typer.echo("\nNote: Full prompt diff requires snapshot storage per revision (not yet available in API).")
3026
+ typer.echo("To investigate: compare guardrail text above against the known-good version,")
2678
3027
  typer.echo(f"and check if any flows were added/removed between {rev_a['created_at'][:10]} and {rev_b['created_at'][:10]}.")
2679
3028
 
2680
3029