Add image assets embeddable in artifact content

Agents can now put images inside markdown/HTML artifacts instead of only
posting them as standalone artifacts.

- New Asset model: an image uploaded ahead of the artifact that embeds it.
- POST /api/assets uploads an image (image/* only, 50 MB cap) and returns
  a stable URL the agent references normally, e.g. ![chart](url). No custom
  syntax; existing markdown/fragment rendering handles it.
- GET /assets/{id} serves the image, owner-scoped (404 for other users).
- On artifact creation the content is scanned for asset URLs and matching
  assets are attached to it, so the image dies with the artifact it belongs
  to. Uploads never referenced are swept after a 24h grace period.
- Quota: assets count toward the owner's quota at upload time and can
  trigger eviction; evicting an artifact frees its assets too, and usage
  now sums artifacts plus assets.
- daybook_post.py gains --upload-image, which prints the URL to embed.
- Docs and SKILL.md cover the upload-then-reference workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ritte 2026-07-24 12:24:51 +02:00
parent 13f728bcd6
commit bf8d1fe244
9 changed files with 316 additions and 10 deletions

View file

@ -45,6 +45,23 @@ DAYBOOK_SOURCE=agent
For images, PDFs, SVGs, and other files, use `--file`; the helper infers multipart and content type.
## Images Inside Content
To embed an image *within* a markdown or HTML artifact (rather than posting it as its own
artifact), upload it first and reference the returned URL:
```bash
# 1. Upload -> prints the URL as the last line
python skills/daybook-posting/scripts/daybook_post.py --upload-image chart.png
# 2. Reference that URL in your markdown, then post as usual
# ![chart](http://127.0.0.1:8001/assets/ab12cd34ef56)
python skills/daybook-posting/scripts/daybook_post.py report.md
```
The image is linked to the artifact that references it and is deleted along with it.
Unreferenced uploads are cleaned up after 24 hours.
For CSV, JSON, or code that should render today, use `--file path --as-markdown` to post it as a Markdown fenced code block. Without `--as-markdown`, these upload as downloadable binary files.
## Rules

View file

@ -49,6 +49,19 @@ def main():
)
return 2
if args.upload_image:
try:
response = upload_asset(base_url, token, Path(args.upload_image))
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
except (HTTPError, URLError) as exc:
print_http_error(exc)
return 1
print(json.dumps(response, indent=2))
print(response.get("url", ""))
return 0
try:
if args.path:
resolve_auto_path(args)
@ -87,11 +100,12 @@ def parse_args():
parser.add_argument("--source-prompt", default="", help="Prompt/instruction that produced the artifact.")
parser.add_argument("--source-type", choices=["agent", "person", "system"], default="agent")
parser.add_argument("--as-markdown", action="store_true", help="Wrap --file content in a Markdown fenced code block instead of uploading it.")
parser.add_argument("--upload-image", help="Upload an image for embedding in artifact content. Prints the URL to reference in markdown/HTML.")
parser.add_argument("--check", action="store_true", help="Fetch /api/capabilities and exit.")
parser.add_argument("--dry-run", action="store_true", help="Print request details without posting.")
args = parser.parse_args()
if args.check:
if args.check or args.upload_image:
return args
if args.path:
@ -370,6 +384,28 @@ def content_type_for(path):
return mimetypes.guess_type(path.name)[0] or "application/octet-stream"
def upload_asset(base_url, token, path):
"""Upload an image and return its Daybook URL for embedding in content."""
if not path.is_file():
raise ValueError(f"image file does not exist: {path}")
boundary = f"----daybook-{uuid.uuid4().hex}"
chunks = [
f"--{boundary}\r\n".encode("utf-8"),
f'Content-Disposition: form-data; name="file"; filename="{path.name}"\r\n'.encode("utf-8"),
f"Content-Type: {content_type_for(path)}\r\n\r\n".encode("utf-8"),
path.read_bytes(),
b"\r\n",
f"--{boundary}--\r\n".encode("utf-8"),
]
request = Request(f"{base_url}/api/assets", data=b"".join(chunks), method="POST")
request.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
request.add_header("Authorization", f"Bearer {token}")
request.add_header("Accept", "application/json")
with urlopen(request, timeout=60) as response:
return json.loads(response.read().decode("utf-8"))
def request_json(method, url, token=""):
request = Request(url, method=method)
request.add_header("Accept", "application/json")