cf-agent 0.1.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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ *.pyd
5
+ *.so
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .venv/
10
+ .env
11
+ .DS_Store
cf_agent-0.1.0/LICENSE ADDED
@@ -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.
@@ -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,50 @@
1
+ # cf-agent
2
+
3
+ AI agent CLI for managing Adobe AEM Content Fragments with natural language.
4
+
5
+ ## Features
6
+
7
+ - Chat-driven operations for content fragments
8
+ - Login command to configure Adobe and Anthropic credentials
9
+ - Non-interactive mode for scripting (`--message`)
10
+
11
+ ## Installation
12
+
13
+ Install from PyPI (after publishing):
14
+
15
+ ```bash
16
+ pip install cf-agent
17
+ ```
18
+
19
+ Install from a local wheel:
20
+
21
+ ```bash
22
+ pip install dist/cf_agent-0.1.0-py3-none-any.whl
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ cf-agent login
29
+ cf-agent chat
30
+ ```
31
+
32
+ Run a single command non-interactively:
33
+
34
+ ```bash
35
+ cf-agent chat --message "list fragments in /content/dam/my-project"
36
+ ```
37
+
38
+ ## Development Build
39
+
40
+ ```bash
41
+ python -m pip install --upgrade build
42
+ python -m build
43
+ ```
44
+
45
+ Generated artifacts will be in `dist/`.
46
+
47
+ ## License
48
+
49
+ MIT
50
+ # aem-cs-cli
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -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()
@@ -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"]
@@ -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()
@@ -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)
@@ -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,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cf-agent"
7
+ version = "0.1.0"
8
+ description = "AI agent for Adobe AEM Content Fragments"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "CF Agent Maintainers" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ ]
26
+ dependencies = [
27
+ "anthropic>=0.40.0",
28
+ "httpx>=0.27.0",
29
+ "click>=8.1.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/your-org/cf-agent"
34
+ Repository = "https://github.com/your-org/cf-agent"
35
+ Issues = "https://github.com/your-org/cf-agent/issues"
36
+
37
+ [project.scripts]
38
+ cf-agent = "cf_agent.agent:cli"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["cf_agent"]