cf-agent 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.
- cf_agent/__init__.py +1 -0
- cf_agent/agent.py +147 -0
- cf_agent/auth.py +32 -0
- cf_agent/client.py +27 -0
- cf_agent/config.py +74 -0
- cf_agent/tools.py +233 -0
- cf_agent-0.1.0.dist-info/METADATA +75 -0
- cf_agent-0.1.0.dist-info/RECORD +11 -0
- cf_agent-0.1.0.dist-info/WHEEL +4 -0
- cf_agent-0.1.0.dist-info/entry_points.txt +2 -0
- cf_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
cf_agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
cf_agent/agent.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""CLI entry point and agent loop."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import click
|
|
5
|
+
import anthropic
|
|
6
|
+
|
|
7
|
+
from . import config as cfg_module
|
|
8
|
+
from . import tools
|
|
9
|
+
|
|
10
|
+
SYSTEM = """\
|
|
11
|
+
You are a helpful assistant for managing Adobe AEM Content Fragments.
|
|
12
|
+
You have tools to list, search, get, create, update, delete, publish, and copy fragments,
|
|
13
|
+
as well as list models and variations.
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
- Always confirm before deleting or bulk-publishing unless the user already confirmed
|
|
17
|
+
- When updating or deleting, always call get_fragment first to obtain the current ETag
|
|
18
|
+
- Summarise results concisely — show counts and key fields, not raw JSON, unless asked
|
|
19
|
+
- For bulk results show the first 5 and summarise the rest
|
|
20
|
+
- If an API call returns an error, explain it clearly in plain English
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _agent_loop(cfg: dict, user_input: str, messages: list) -> list:
|
|
25
|
+
"""Run one user turn through the agentic loop. Returns updated messages."""
|
|
26
|
+
client = anthropic.Anthropic()
|
|
27
|
+
messages.append({"role": "user", "content": user_input})
|
|
28
|
+
|
|
29
|
+
while True:
|
|
30
|
+
response = client.messages.create(
|
|
31
|
+
model="claude-sonnet-4-6",
|
|
32
|
+
max_tokens=4096,
|
|
33
|
+
system=SYSTEM,
|
|
34
|
+
tools=tools.DEFINITIONS,
|
|
35
|
+
messages=messages,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
text_parts = [b.text for b in response.content if hasattr(b, "text")]
|
|
39
|
+
tool_calls = [b for b in response.content if b.type == "tool_use"]
|
|
40
|
+
|
|
41
|
+
if text_parts:
|
|
42
|
+
click.echo(f"\nAgent: {''.join(text_parts)}\n")
|
|
43
|
+
|
|
44
|
+
if not tool_calls or response.stop_reason == "end_turn":
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
48
|
+
|
|
49
|
+
tool_results = []
|
|
50
|
+
for tc in tool_calls:
|
|
51
|
+
click.echo(f" ↳ {tc.name}({json.dumps(tc.input, separators=(',', ':'))})")
|
|
52
|
+
try:
|
|
53
|
+
result = tools.HANDLERS[tc.name](cfg, **tc.input)
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
result = {"error": str(exc)}
|
|
56
|
+
tool_results.append({
|
|
57
|
+
"type": "tool_result",
|
|
58
|
+
"tool_use_id": tc.id,
|
|
59
|
+
"content": json.dumps(result),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
messages.append({"role": "user", "content": tool_results})
|
|
63
|
+
|
|
64
|
+
return messages
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── CLI commands ───────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
@click.group()
|
|
70
|
+
def cli():
|
|
71
|
+
"""Content Fragment AI Agent — manage Adobe AEM fragments with natural language."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@cli.command()
|
|
75
|
+
def login():
|
|
76
|
+
"""Configure your Adobe and Anthropic credentials."""
|
|
77
|
+
click.echo("Enter your credentials (press Enter to keep existing value):\n")
|
|
78
|
+
|
|
79
|
+
existing = cfg_module._parse_dotenv(cfg_module.CONFIG_FILE)
|
|
80
|
+
|
|
81
|
+
fields = {
|
|
82
|
+
"ADOBE_CLIENT_ID": "Adobe Client ID",
|
|
83
|
+
"ADOBE_CLIENT_SECRET": "Adobe Client Secret",
|
|
84
|
+
"ADOBE_IMS_TOKEN_URL": "Adobe IMS Token URL",
|
|
85
|
+
"ADOBE_SITES_API_BASE_URL": "Adobe Sites API Base URL",
|
|
86
|
+
"ADOBE_SCOPES": "Adobe Scopes",
|
|
87
|
+
"ANTHROPIC_API_KEY": "Anthropic API Key",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
defaults = {
|
|
91
|
+
"ADOBE_IMS_TOKEN_URL": "https://ims-na1.adobelogin.com/ims/token/v3",
|
|
92
|
+
"ADOBE_SCOPES": "openid,AdobeID,aem.fragments.management,aem.folders",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
values = {}
|
|
96
|
+
for key, label in fields.items():
|
|
97
|
+
current = existing.get(key, defaults.get(key, ""))
|
|
98
|
+
prompt = f"{label}"
|
|
99
|
+
if current:
|
|
100
|
+
display = current[:6] + "..." if len(current) > 8 else current
|
|
101
|
+
prompt += f" [{display}]"
|
|
102
|
+
value = click.prompt(prompt, default=current, show_default=False)
|
|
103
|
+
values[key] = value.strip()
|
|
104
|
+
|
|
105
|
+
cfg_module.save(values)
|
|
106
|
+
click.echo(f"\nCredentials saved to {cfg_module.CONFIG_FILE}")
|
|
107
|
+
click.echo("Run `cf-agent chat` to start.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@cli.command()
|
|
111
|
+
@click.option("--message", "-m", default=None, help="Run a single message non-interactively.")
|
|
112
|
+
def chat(message):
|
|
113
|
+
"""Start an interactive chat session with the Content Fragment agent."""
|
|
114
|
+
cfg = cfg_module.load()
|
|
115
|
+
messages = []
|
|
116
|
+
|
|
117
|
+
if message:
|
|
118
|
+
# Non-interactive: run one message and exit (useful for scripting)
|
|
119
|
+
_agent_loop(cfg, message, messages)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
click.echo("Content Fragment Agent — type 'quit' to exit.\n")
|
|
123
|
+
|
|
124
|
+
while True:
|
|
125
|
+
try:
|
|
126
|
+
user_input = click.prompt("You", prompt_suffix=": ").strip()
|
|
127
|
+
except (click.exceptions.Abort, EOFError):
|
|
128
|
+
click.echo("\nGoodbye.")
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
if user_input.lower() in ("quit", "exit", "q"):
|
|
132
|
+
click.echo("Goodbye.")
|
|
133
|
+
break
|
|
134
|
+
if not user_input:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
messages = _agent_loop(cfg, user_input, messages)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Allow `cf-agent` with no subcommand to start chat directly
|
|
141
|
+
@cli.result_callback()
|
|
142
|
+
def _default(*args, **kwargs):
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
cli()
|
cf_agent/auth.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Adobe IMS token management with in-memory caching."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
_cache: dict = {"token": None, "expires_at": 0.0}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_token(cfg: dict) -> str:
|
|
10
|
+
"""Return a valid access token, fetching a fresh one when expired."""
|
|
11
|
+
if _cache["token"] and time.time() < _cache["expires_at"] - 60:
|
|
12
|
+
return _cache["token"]
|
|
13
|
+
|
|
14
|
+
resp = httpx.post(
|
|
15
|
+
cfg["ADOBE_IMS_TOKEN_URL"],
|
|
16
|
+
data={
|
|
17
|
+
"client_id": cfg["ADOBE_CLIENT_ID"],
|
|
18
|
+
"client_secret": cfg["ADOBE_CLIENT_SECRET"],
|
|
19
|
+
"grant_type": "client_credentials",
|
|
20
|
+
"scope": cfg["ADOBE_SCOPES"],
|
|
21
|
+
},
|
|
22
|
+
timeout=30,
|
|
23
|
+
)
|
|
24
|
+
resp.raise_for_status()
|
|
25
|
+
data = resp.json()
|
|
26
|
+
|
|
27
|
+
if "access_token" not in data:
|
|
28
|
+
raise RuntimeError(f"Token fetch failed: {data}")
|
|
29
|
+
|
|
30
|
+
_cache["token"] = data["access_token"]
|
|
31
|
+
_cache["expires_at"] = time.time() + data.get("expires_in", 86400)
|
|
32
|
+
return _cache["token"]
|
cf_agent/client.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Thin authenticated HTTP client for the Adobe Sites API."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from .auth import get_token
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def request(cfg: dict, method: str, path: str, content_type: str = "application/json", **kwargs) -> dict:
|
|
8
|
+
"""Make an authenticated call to the Adobe Sites API."""
|
|
9
|
+
headers = {
|
|
10
|
+
"Authorization": f"Bearer {get_token(cfg)}",
|
|
11
|
+
"Accept": "application/json",
|
|
12
|
+
**kwargs.pop("headers", {}),
|
|
13
|
+
}
|
|
14
|
+
if method in ("POST", "PUT", "PATCH"):
|
|
15
|
+
headers["Content-Type"] = content_type
|
|
16
|
+
|
|
17
|
+
resp = httpx.request(
|
|
18
|
+
method,
|
|
19
|
+
f"{cfg['ADOBE_SITES_API_BASE_URL']}{path}",
|
|
20
|
+
headers=headers,
|
|
21
|
+
timeout=30,
|
|
22
|
+
**kwargs,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if resp.status_code == 204 or not resp.content:
|
|
26
|
+
return {"status": "success", "message": "Operation completed successfully"}
|
|
27
|
+
return resp.json()
|
cf_agent/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config loader. Reads credentials in this priority order:
|
|
3
|
+
1. Environment variables
|
|
4
|
+
2. ~/.cf-agent/config (written by `cf-agent login`)
|
|
5
|
+
3. .env in current working directory
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
REQUIRED = [
|
|
12
|
+
"ADOBE_CLIENT_ID",
|
|
13
|
+
"ADOBE_CLIENT_SECRET",
|
|
14
|
+
"ADOBE_IMS_TOKEN_URL",
|
|
15
|
+
"ADOBE_SITES_API_BASE_URL",
|
|
16
|
+
"ADOBE_SCOPES",
|
|
17
|
+
"ANTHROPIC_API_KEY",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
CONFIG_DIR = Path.home() / ".cf-agent"
|
|
21
|
+
CONFIG_FILE = CONFIG_DIR / "config"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_dotenv(path: Path) -> dict:
|
|
25
|
+
result = {}
|
|
26
|
+
try:
|
|
27
|
+
with open(path) as f:
|
|
28
|
+
for line in f:
|
|
29
|
+
line = line.strip().rstrip("\r")
|
|
30
|
+
if not line or line.startswith("#"):
|
|
31
|
+
continue
|
|
32
|
+
key, _, value = line.partition("=")
|
|
33
|
+
result[key.strip()] = value.strip()
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
pass
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load() -> dict:
|
|
40
|
+
"""Return a config dict, merging all sources. Raises if required keys are missing."""
|
|
41
|
+
merged = {}
|
|
42
|
+
|
|
43
|
+
# Lowest priority: .env in cwd
|
|
44
|
+
merged.update(_parse_dotenv(Path.cwd() / ".env"))
|
|
45
|
+
|
|
46
|
+
# Mid priority: ~/.cf-agent/config
|
|
47
|
+
merged.update(_parse_dotenv(CONFIG_FILE))
|
|
48
|
+
|
|
49
|
+
# Highest priority: real environment variables
|
|
50
|
+
for key in REQUIRED:
|
|
51
|
+
if key in os.environ:
|
|
52
|
+
merged[key] = os.environ[key]
|
|
53
|
+
|
|
54
|
+
missing = [k for k in REQUIRED if not merged.get(k)]
|
|
55
|
+
if missing:
|
|
56
|
+
raise SystemExit(
|
|
57
|
+
f"Missing configuration: {', '.join(missing)}\n"
|
|
58
|
+
"Run `cf-agent login` to set up your credentials."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Inject into os.environ so the Anthropic SDK picks up ANTHROPIC_API_KEY
|
|
62
|
+
os.environ.setdefault("ANTHROPIC_API_KEY", merged["ANTHROPIC_API_KEY"])
|
|
63
|
+
|
|
64
|
+
return merged
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def save(values: dict) -> None:
|
|
68
|
+
"""Persist credentials to ~/.cf-agent/config."""
|
|
69
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
CONFIG_FILE.chmod(0o700) if CONFIG_FILE.exists() else None
|
|
71
|
+
with open(CONFIG_FILE, "w") as f:
|
|
72
|
+
for k, v in values.items():
|
|
73
|
+
f.write(f"{k}={v}\n")
|
|
74
|
+
CONFIG_FILE.chmod(0o600)
|
cf_agent/tools.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Tool definitions and handlers for the Content Fragment agent."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from . import client as api_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ── Handlers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
def list_fragments(cfg, path=None, limit=20, projection="summary"):
|
|
11
|
+
params = {"limit": min(limit, 50), "projection": projection}
|
|
12
|
+
if path:
|
|
13
|
+
params["path"] = path
|
|
14
|
+
return api_client.request(cfg, "GET", "/cf/fragments", params=params)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_fragment(cfg, fragment_id):
|
|
18
|
+
return api_client.request(cfg, "GET", f"/cf/fragments/{fragment_id}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def search_fragments(cfg, query_filter, limit=20, projection="summary"):
|
|
22
|
+
query = urllib.parse.quote(json.dumps({"filter": query_filter}))
|
|
23
|
+
params = {"query": query, "limit": min(limit, 50), "projection": projection}
|
|
24
|
+
return api_client.request(cfg, "GET", "/cf/fragments/search", params=params)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_fragment(cfg, title, model_id, parent_path, description=None, fields=None):
|
|
28
|
+
body = {"title": title, "modelId": model_id, "parentPath": parent_path}
|
|
29
|
+
if description:
|
|
30
|
+
body["description"] = description
|
|
31
|
+
if fields:
|
|
32
|
+
body["fields"] = fields
|
|
33
|
+
return api_client.request(cfg, "POST", "/cf/fragments", json=body)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def update_fragment(cfg, fragment_id, etag, patch_ops):
|
|
37
|
+
return api_client.request(
|
|
38
|
+
cfg,
|
|
39
|
+
"PATCH",
|
|
40
|
+
f"/cf/fragments/{fragment_id}",
|
|
41
|
+
json=patch_ops,
|
|
42
|
+
headers={"If-Match": etag},
|
|
43
|
+
content_type="application/json-patch+json",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def delete_fragment(cfg, fragment_id, etag):
|
|
48
|
+
return api_client.request(
|
|
49
|
+
cfg, "DELETE", f"/cf/fragments/{fragment_id}", headers={"If-Match": etag}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def publish_fragments(cfg, ids=None, paths=None):
|
|
54
|
+
body = {"filterReferencesByStatus": ["DRAFT", "MODIFIED"]}
|
|
55
|
+
if ids:
|
|
56
|
+
body["ids"] = ids
|
|
57
|
+
if paths:
|
|
58
|
+
body["paths"] = paths
|
|
59
|
+
return api_client.request(cfg, "POST", "/cf/fragments/publish", json=body)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_models(cfg, limit=20, projection="summary"):
|
|
63
|
+
return api_client.request(
|
|
64
|
+
cfg, "GET", "/cf/models", params={"limit": min(limit, 50), "projection": projection}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def list_variations(cfg, fragment_id):
|
|
69
|
+
return api_client.request(cfg, "GET", f"/cf/fragments/{fragment_id}/variations")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def copy_fragment(cfg, fragment_id, parent_path, name=None):
|
|
73
|
+
body = {"parentPath": parent_path}
|
|
74
|
+
if name:
|
|
75
|
+
body["name"] = name
|
|
76
|
+
return api_client.request(cfg, "POST", f"/cf/fragments/{fragment_id}/copy", json=body)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Tool definitions (Anthropic format) ───────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
DEFINITIONS = [
|
|
82
|
+
{
|
|
83
|
+
"name": "list_fragments",
|
|
84
|
+
"description": "List content fragments, optionally filtered by a DAM path.",
|
|
85
|
+
"input_schema": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"path": {"type": "string", "description": "DAM path prefix, e.g. /content/dam/marketplace"},
|
|
89
|
+
"limit": {"type": "integer", "default": 20, "description": "Max results (1-50)"},
|
|
90
|
+
"projection": {"type": "string", "enum": ["minimal", "summary", "full"], "default": "summary"},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "get_fragment",
|
|
96
|
+
"description": "Get full details of a content fragment by UUID. Always call this before updating or deleting to obtain the current ETag.",
|
|
97
|
+
"input_schema": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {
|
|
100
|
+
"fragment_id": {"type": "string", "description": "The UUID of the content fragment"},
|
|
101
|
+
},
|
|
102
|
+
"required": ["fragment_id"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "search_fragments",
|
|
107
|
+
"description": "Search fragments using filters: status, model, path, or full text.",
|
|
108
|
+
"input_schema": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"properties": {
|
|
111
|
+
"query_filter": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"description": (
|
|
114
|
+
"Filter criteria. Supported keys: "
|
|
115
|
+
"path (string), "
|
|
116
|
+
"status (array: NEW|DRAFT|PUBLISHED|MODIFIED|UNPUBLISHED), "
|
|
117
|
+
"modelIds (array of base64 model IDs), "
|
|
118
|
+
"fullText ({text: string, queryMode: AS_IS|EXACT_WORDS|EXACT_PHRASE})"
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
"limit": {"type": "integer", "default": 20},
|
|
122
|
+
"projection": {"type": "string", "enum": ["minimal", "summary", "full"], "default": "summary"},
|
|
123
|
+
},
|
|
124
|
+
"required": ["query_filter"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "create_fragment",
|
|
129
|
+
"description": "Create a new content fragment under a DAM folder.",
|
|
130
|
+
"input_schema": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"title": {"type": "string"},
|
|
134
|
+
"model_id": {"type": "string", "description": "Base64-encoded model ID (use list_models to find it)"},
|
|
135
|
+
"parent_path": {"type": "string", "description": "Parent folder, e.g. /content/dam/marketplace"},
|
|
136
|
+
"description": {"type": "string"},
|
|
137
|
+
"fields": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"description": "Initial field values, e.g. [{name: 'title', type: 'text', values: ['Hello']}]",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
"required": ["title", "model_id", "parent_path"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "update_fragment",
|
|
147
|
+
"description": "Update a fragment using JSON Patch operations. Call get_fragment first to get the ETag.",
|
|
148
|
+
"input_schema": {
|
|
149
|
+
"type": "object",
|
|
150
|
+
"properties": {
|
|
151
|
+
"fragment_id": {"type": "string"},
|
|
152
|
+
"etag": {"type": "string", "description": "ETag from get_fragment"},
|
|
153
|
+
"patch_ops": {
|
|
154
|
+
"type": "array",
|
|
155
|
+
"description": "JSON Patch ops, e.g. [{op: 'replace', path: '/title', value: 'New Title'}]",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
"required": ["fragment_id", "etag", "patch_ops"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "delete_fragment",
|
|
163
|
+
"description": "Delete a content fragment. Must not be PUBLISHED. Call get_fragment first to get the ETag.",
|
|
164
|
+
"input_schema": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"properties": {
|
|
167
|
+
"fragment_id": {"type": "string"},
|
|
168
|
+
"etag": {"type": "string"},
|
|
169
|
+
},
|
|
170
|
+
"required": ["fragment_id", "etag"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "publish_fragments",
|
|
175
|
+
"description": "Publish one or more content fragments by UUID or DAM path.",
|
|
176
|
+
"input_schema": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"ids": {"type": "array", "items": {"type": "string"}, "description": "Fragment UUIDs"},
|
|
180
|
+
"paths": {"type": "array", "items": {"type": "string"}, "description": "Fragment DAM paths"},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "list_models",
|
|
186
|
+
"description": "List available content fragment models. Use this to find model IDs when creating fragments.",
|
|
187
|
+
"input_schema": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"properties": {
|
|
190
|
+
"limit": {"type": "integer", "default": 20},
|
|
191
|
+
"projection": {"type": "string", "enum": ["minimal", "summary", "full"], "default": "summary"},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "list_variations",
|
|
197
|
+
"description": "List all variations of a content fragment.",
|
|
198
|
+
"input_schema": {
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": {
|
|
201
|
+
"fragment_id": {"type": "string"},
|
|
202
|
+
},
|
|
203
|
+
"required": ["fragment_id"],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "copy_fragment",
|
|
208
|
+
"description": "Copy a content fragment to a different DAM folder.",
|
|
209
|
+
"input_schema": {
|
|
210
|
+
"type": "object",
|
|
211
|
+
"properties": {
|
|
212
|
+
"fragment_id": {"type": "string", "description": "UUID of the fragment to copy"},
|
|
213
|
+
"parent_path": {"type": "string", "description": "Destination folder path"},
|
|
214
|
+
"name": {"type": "string", "description": "Optional new name for the copy"},
|
|
215
|
+
},
|
|
216
|
+
"required": ["fragment_id", "parent_path"],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
# Maps tool name → handler function (cfg is injected by the agent loop)
|
|
222
|
+
HANDLERS = {
|
|
223
|
+
"list_fragments": list_fragments,
|
|
224
|
+
"get_fragment": get_fragment,
|
|
225
|
+
"search_fragments": search_fragments,
|
|
226
|
+
"create_fragment": create_fragment,
|
|
227
|
+
"update_fragment": update_fragment,
|
|
228
|
+
"delete_fragment": delete_fragment,
|
|
229
|
+
"publish_fragments": publish_fragments,
|
|
230
|
+
"list_models": list_models,
|
|
231
|
+
"list_variations": list_variations,
|
|
232
|
+
"copy_fragment": copy_fragment,
|
|
233
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cf-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI agent for Adobe AEM Content Fragments
|
|
5
|
+
Project-URL: Homepage, https://github.com/your-org/cf-agent
|
|
6
|
+
Project-URL: Repository, https://github.com/your-org/cf-agent
|
|
7
|
+
Project-URL: Issues, https://github.com/your-org/cf-agent/issues
|
|
8
|
+
Author: CF Agent Maintainers
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: anthropic>=0.40.0
|
|
22
|
+
Requires-Dist: click>=8.1.0
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# cf-agent
|
|
27
|
+
|
|
28
|
+
AI agent CLI for managing Adobe AEM Content Fragments with natural language.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- Chat-driven operations for content fragments
|
|
33
|
+
- Login command to configure Adobe and Anthropic credentials
|
|
34
|
+
- Non-interactive mode for scripting (`--message`)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Install from PyPI (after publishing):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install cf-agent
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Install from a local wheel:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install dist/cf_agent-0.1.0-py3-none-any.whl
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cf-agent login
|
|
54
|
+
cf-agent chat
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Run a single command non-interactively:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cf-agent chat --message "list fragments in /content/dam/my-project"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Development Build
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m pip install --upgrade build
|
|
67
|
+
python -m build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Generated artifacts will be in `dist/`.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
75
|
+
# aem-cs-cli
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
cf_agent/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
cf_agent/agent.py,sha256=tj2G_PTB_NitZycq8ggPwV2nONOGSDxOt2o94oM1bAE,4878
|
|
3
|
+
cf_agent/auth.py,sha256=_PBV_OBbatEvUvBg7hhnGZrwKoLc0Fc3UgRyk5N9etw,937
|
|
4
|
+
cf_agent/client.py,sha256=jQfhSx5CyHwY4DkQ4xOAnQc2rn-0e4A7cJOHOMiwkV8,837
|
|
5
|
+
cf_agent/config.py,sha256=YsCr2DSkNqRaauDzqVj7_R3WmI5o8TMeUVyn4G7H1nQ,2084
|
|
6
|
+
cf_agent/tools.py,sha256=-HuOKLXdUgG2ITCDllmWDWhPY-hSaX_85cXVoffoDDU,8978
|
|
7
|
+
cf_agent-0.1.0.dist-info/METADATA,sha256=D8cecCDEpp95wE0J2-KdejJMc23D4jCkdPfv5jgQyO8,1754
|
|
8
|
+
cf_agent-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
cf_agent-0.1.0.dist-info/entry_points.txt,sha256=UB9XIt5_0t60UoLIUNc0w4tdj4zHtJjUGXvlhB1EmGM,48
|
|
10
|
+
cf_agent-0.1.0.dist-info/licenses/LICENSE,sha256=Z59M7gyMzSjufiJpgmYRkDH-4TGc27qkyPBiPKZLC5k,1077
|
|
11
|
+
cf_agent-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CF Agent Maintainers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|