Compare commits

...

2 commits

Author SHA1 Message Date
fb69261218 Add GET /api/skill endpoint serving the posting skill
Serves skills/daybook-posting/SKILL.md so users/agents can fetch the
posting instructions from the running instance instead of hunting for the
file. The server rewrites the DAYBOOK_URL config line to the request's own
origin, so the text handed to an agent already points at the right server;
the recipient only supplies their token.

- Same auth as /api/capabilities (session or bearer token); no user data.
- Raw text/markdown by default, ?format=json returns {version, url, skill}.
- Advertised via skill_url + a /api/skill methods entry in capabilities.
- Ships in the Docker image (COPY . /app, skills/ not dockerignored).
- Tests for auth, origin rewrite, JSON mode, session auth, capabilities ad.
- docs/api.md + CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 15:50:04 +01:00
caeb2bedc7 Rewrite posting skill to be location-agnostic
The skill was authored in this dev workspace, where the Daybook server,
the skill, and the posting agent all share one directory — so it read as
if the agent always runs from a Daybook source checkout on localhost.

- State up front that you post to a running instance that may be anywhere,
  need no source checkout, and are tied to it only via DAYBOOK_URL/TOKEN.
- Drop "from the project root"; reference the bundled helper by absolute
  path (DAYBOOK_POST=.../this-skill/scripts/daybook_post.py).
- Reframe DAYBOOK_URL as set-this-to-your-instance; label 127.0.0.1:8001
  as the local dev fallback only.
- Genericize the hardcoded localhost URL in the image-embed example.
- Add a raw curl fallback for environments without the Python helper.
- Keep frontmatter to name+description (the Skills-API-portable subset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 15:43:58 +01:00
6 changed files with 216 additions and 33 deletions

View file

@ -6,7 +6,16 @@ the git tags (`vX.Y.Z`) and `daybook_project.__version__`.
## [Unreleased] ## [Unreleased]
_Nothing yet._ ### Added
- `GET /api/skill` serves the bundled agent-posting skill as Markdown (or
`?format=json`), with `DAYBOOK_URL` pre-filled to the instance's own
origin so the instructions are ready to hand to an AI. Advertised as
`skill_url` in `/api/capabilities`.
### Changed
- Rewrote the posting skill (`skills/daybook-posting/SKILL.md`) to be
location-agnostic: no longer assumes a local source checkout, reframes
`DAYBOOK_URL` as set-your-instance, and adds a raw `curl` fallback.
## [0.1.0] — 2026-09-03 ## [0.1.0] — 2026-09-03

View file

@ -14,6 +14,7 @@ urlpatterns = [
path("assets/<str:asset_id>", views.asset_view, name="asset"), path("assets/<str:asset_id>", views.asset_view, name="asset"),
path("api/assets", views.AssetUploadView.as_view(), name="asset-upload-api"), path("api/assets", views.AssetUploadView.as_view(), name="asset-upload-api"),
path("api/capabilities", views.CapabilitiesView.as_view(), name="capabilities-api"), path("api/capabilities", views.CapabilitiesView.as_view(), name="capabilities-api"),
path("api/skill", views.SkillView.as_view(), name="skill-api"),
path("api/artifacts", views.ArtifactListCreateView.as_view(), name="artifact-api"), path("api/artifacts", views.ArtifactListCreateView.as_view(), name="artifact-api"),
path("api/artifacts/<str:artifact_id>", views.ArtifactDetailView.as_view(), name="artifact-detail-api"), path("api/artifacts/<str:artifact_id>", views.ArtifactDetailView.as_view(), name="artifact-detail-api"),
path("settings/", views.settings_home, name="settings-home"), path("settings/", views.settings_home, name="settings-home"),

View file

@ -322,11 +322,57 @@ class CapabilitiesView(APIView):
"/api/artifacts": ["GET", "POST"], "/api/artifacts": ["GET", "POST"],
"/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"],
"/api/assets": ["POST"], "/api/assets": ["POST"],
"/api/capabilities": ["GET"],
"/api/skill": ["GET"],
}, },
"skill_url": request.build_absolute_uri(reverse("skill-api")),
} }
) )
SKILL_MD_PATH = settings.BASE_DIR / "skills" / "daybook-posting" / "SKILL.md"
def render_posting_skill(origin):
"""Return the bundled posting SKILL.md with its DAYBOOK_URL placeholder
rewritten to this instance's origin, so text handed to an agent already
points at the right server. Only the config-block assignment is touched;
the surrounding prose (which explains DAYBOOK_URL) is left alone."""
text = SKILL_MD_PATH.read_text(encoding="utf-8")
return re.sub(
r"(?m)^(DAYBOOK_URL=)\S+",
lambda m: m.group(1) + origin,
text,
)
class SkillView(APIView):
"""Serve the bundled agent-posting skill (skills/daybook-posting/SKILL.md),
with DAYBOOK_URL pre-filled to this instance so a recipient posts to the
right server. Same auth as capabilities: a logged-in user can copy it, or
an agent can fetch it with its API token.
GET /api/skill -> raw Markdown (text/markdown)
GET /api/skill?format=json -> {"version", "url", "skill"}
"""
def get(self, request):
if not request.user.is_authenticated and resolve_token_user(request) is None:
return HttpResponseForbidden("Login or a valid API token is required")
origin = request.build_absolute_uri("/").rstrip("/")
try:
skill = render_posting_skill(origin)
except FileNotFoundError:
raise Http404("Posting skill is not bundled with this deployment")
if request.GET.get("format") == "json":
from daybook_project import __version__
return Response({"version": __version__, "url": origin, "skill": skill})
return HttpResponse(skill, content_type="text/markdown; charset=utf-8")
FEED_WINDOW_DAYS = 7 FEED_WINDOW_DAYS = 7

View file

@ -529,8 +529,11 @@ Example response:
"methods": { "methods": {
"/api/artifacts": ["GET", "POST"], "/api/artifacts": ["GET", "POST"],
"/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"],
"/api/assets": ["POST"] "/api/assets": ["POST"],
} "/api/capabilities": ["GET"],
"/api/skill": ["GET"]
},
"skill_url": "http://127.0.0.1:8001/api/skill"
} }
``` ```
@ -545,6 +548,33 @@ endpoints, it accepts a bearer token as well as a browser session:
curl -H "Authorization: Bearer <your-personal-token>" http://127.0.0.1:8001/api/capabilities curl -H "Authorization: Bearer <your-personal-token>" http://127.0.0.1:8001/api/capabilities
``` ```
## Posting Skill
Endpoint:
```http
GET /api/skill
GET /api/skill?format=json
```
Returns the bundled agent-posting skill (`skills/daybook-posting/SKILL.md`) —
the same instructions you'd give an AI so it knows how to publish to this
instance. The server rewrites the `DAYBOOK_URL` line to **this instance's own
origin**, so the text a caller receives already points at the right server;
a recipient only has to fill in their `DAYBOOK_TOKEN`.
Same auth as capabilities (a browser session **or** a bearer token), so a
logged-in user can copy it to hand to their agent, or an agent can fetch it
directly. The default response is raw Markdown (`text/markdown`); add
`?format=json` for `{"version", "url", "skill"}`.
```bash
# Fetch the ready-to-use instructions for this instance
curl -H "Authorization: Bearer <your-personal-token>" http://127.0.0.1:8001/api/skill
```
Discover it programmatically via `skill_url` in the capabilities response.
## Current MVP Constraints ## Current MVP Constraints
- Each user has a personal ingest API token; there is no shared token. - Each user has a personal ingest API token; there is no shared token.

View file

@ -1,26 +1,13 @@
--- ---
name: daybook-posting name: daybook-posting
description: Use when you have produced a finished deliverable such as a report, summary, brief, diagram, generated HTML, PDF, deck, or status update that a human should review later outside this chat. Publishes the artifact to Daybook and returns a URL. description: Use when you have produced a finished deliverable such as a report, summary, brief, diagram, generated HTML, PDF, deck, or status update that a human should review later outside this chat. Publishes the artifact to a Daybook instance and returns a URL.
--- ---
# Daybook Posting # Daybook Posting
Daybook is a private artifact journal for completed agent outputs. It stores, indexes, and displays artifacts for human review. It does not execute commands, run workflows, or continue conversations. Daybook is a private artifact journal for completed agent outputs. It stores, indexes, and displays artifacts for human review. It does not execute commands, run workflows, or continue conversations.
Always use the helper script. Do not hand-build curl, JSON, PowerShell multipart, or inline shell content. You post to a **running Daybook server**, which may be anywhere — a cloud VM, a container, or a workstation. You do **not** need a checkout of Daybook's source. The only things that tie you to a particular instance are two environment variables: `DAYBOOK_URL` (where the server is) and `DAYBOOK_TOKEN` (who you are). Set those and the rest of this skill works unchanged regardless of where you or the server run.
From the project root:
```bash
python skills/daybook-posting/scripts/daybook_post.py --check
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" --content-file report.md
cat report.md | python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report"
python skills/daybook-posting/scripts/daybook_post.py --file diagram.svg --title "Architecture Diagram"
```
The project also keeps a compatibility wrapper at `scripts/daybook_post.py`, but the canonical helper is bundled inside this skill.
The artifact URL is printed as the final stdout line.
## Configuration ## Configuration
@ -30,12 +17,54 @@ config) — the helper reads `os.environ` directly. There is no `.env` file
support; a `.env` file in the working directory is not read automatically. support; a `.env` file in the working directory is not read automatically.
```text ```text
DAYBOOK_URL=http://127.0.0.1:8001 DAYBOOK_URL=https://your-daybook-instance.example.com # your server's base URL
DAYBOOK_TOKEN=your-personal-api-token DAYBOOK_TOKEN=your-personal-api-token # from Settings -> Account
DAYBOOK_SOURCE=agent DAYBOOK_SOURCE=agent # a stable label for what's posting
``` ```
`DAYBOOK_TOKEN` is your personal per-user API token, from Settings → Account in the app (not a token shared by everyone). Artifacts posted with your token land only in your feed. - **`DAYBOOK_URL`** — the base URL of the Daybook instance you're posting to.
Set it to your server. If it's unset, the helper falls back to
`http://127.0.0.1:8001`, which is only correct when Daybook is running
locally on the same machine (its development default). For any remote or
deployed instance you **must** set this.
- **`DAYBOOK_TOKEN`** — your personal per-user API token, from Settings →
Account in the app (not a token shared by everyone). Artifacts posted with
your token land only in your feed.
- **`DAYBOOK_SOURCE`** — a free-text label for whatever is posting
(agent-profile, script, or person name). Optional; can also be passed per
call with `--source`.
## The Helper
Always use the bundled helper script. Do not hand-build curl, JSON,
PowerShell multipart, or inline shell content (a `curl` fallback for
environments without the helper is documented at the end — the script is
still preferred everywhere it's available).
The helper lives at **`scripts/daybook_post.py` inside this skill's own
directory** — not in any project you happen to be working in. Invoke it by
its absolute path. For brevity, the examples below assume you've captured
that path once:
```bash
# Point this at the helper inside THIS skill directory (absolute path).
DAYBOOK_POST=/abs/path/to/this/skill/scripts/daybook_post.py
```
Then:
```bash
python "$DAYBOOK_POST" --check
python "$DAYBOOK_POST" --kind markdown --title "Build Report" --content-file report.md
cat report.md | python "$DAYBOOK_POST" --kind markdown --title "Build Report"
python "$DAYBOOK_POST" --file diagram.svg --title "Architecture Diagram"
```
The artifact URL is printed as the final stdout line.
(If you *are* working inside a Daybook source checkout, a compatibility
wrapper also exists at `scripts/daybook_post.py` in the repo root, but the
canonical helper is the one bundled in this skill.)
## Kind Cheat Sheet ## Kind Cheat Sheet
@ -65,8 +94,9 @@ presentation.
This script only **creates** artifacts — none of the following are This script only **creates** artifacts — none of the following are
possible through it. Updating an existing artifact's title/tags/metadata, possible through it. Updating an existing artifact's title/tags/metadata,
or deleting one, requires the raw API (see `docs/api.md`); enabling its or deleting one, requires the raw API (see the instance's `docs/api.md` or
public share link is feed-UI-only, with no API equivalent at all yet. `GET /api/capabilities`); enabling its public share link is feed-UI-only,
with no API equivalent at all yet.
## CDN Snippets For `html_full` ## CDN Snippets For `html_full`
@ -119,11 +149,12 @@ artifact), upload it first and reference the returned URL:
```bash ```bash
# 1. Upload -> prints the URL as the last line # 1. Upload -> prints the URL as the last line
python skills/daybook-posting/scripts/daybook_post.py --upload-image chart.png python "$DAYBOOK_POST" --upload-image chart.png
# 2. Reference that URL in your markdown, then post as usual # 2. Reference that URL in your markdown, then post as usual.
# ![chart](http://127.0.0.1:8001/assets/ab12cd34ef56) # The printed URL is absolute against your DAYBOOK_URL, e.g.
python skills/daybook-posting/scripts/daybook_post.py report.md # ![chart]($DAYBOOK_URL/assets/ab12cd34ef56)
python "$DAYBOOK_POST" report.md
``` ```
The image is linked to the artifact that references it and is deleted along with it. The image is linked to the artifact that references it and is deleted along with it.
@ -136,19 +167,19 @@ just post the file positionally; auto-mode detects `.csv`/`.json` and picks
the right kind automatically: the right kind automatically:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py results.csv python "$DAYBOOK_POST" results.csv
python skills/daybook-posting/scripts/daybook_post.py --kind json --title "Run Summary" --content-file summary.json python "$DAYBOOK_POST" --kind json --title "Run Summary" --content-file summary.json
``` ```
`.xlsx` spreadsheets work too, with no special flag needed — just `--file` `.xlsx` spreadsheets work too, with no special flag needed — just `--file`
(or post it positionally, same as any other file): (or post it positionally, same as any other file):
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py balance_sheet.xlsx python "$DAYBOOK_POST" balance_sheet.xlsx
``` ```
The server converts it to a `csv` artifact automatically. Known The server converts it to a `csv` artifact automatically. Known
limitations (see `docs/api.md` for the full list): limitations (see the instance's `docs/api.md` for the full list):
- Only the workbook's **active sheet** converts; other sheets are dropped. - Only the workbook's **active sheet** converts; other sheets are dropped.
- Formula cells need to have been calculated and saved by real spreadsheet - Formula cells need to have been calculated and saved by real spreadsheet
@ -165,7 +196,7 @@ as a binary, use `--file path --as-markdown` to wrap it in a Markdown
fenced code block (still gets syntax highlighting): fenced code block (still gets syntax highlighting):
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --file script.py --as-markdown --title "Generated Script" python "$DAYBOOK_POST" --file script.py --as-markdown --title "Generated Script"
``` ```
Without `--as-markdown`, it uploads as a downloadable binary file instead. Without `--as-markdown`, it uploads as a downloadable binary file instead.
@ -189,8 +220,37 @@ context like `run_id` or `model` — these end up in the artifact's
## Rules ## Rules
- Run `--check` first if unsure Daybook is reachable. - Run `--check` first if unsure Daybook is reachable.
- Make sure `DAYBOOK_URL` points at the right instance and `DAYBOOK_TOKEN` is set before posting anything real.
- Use a stable `DAYBOOK_SOURCE` or pass `--source` (the agent-profile, script, or person name that produced this artifact). - Use a stable `DAYBOOK_SOURCE` or pass `--source` (the agent-profile, script, or person name that produced this artifact).
- Use `--tags a,b,c` for filtering. - Use `--tags a,b,c` for filtering.
- Use `--meta run_id=... --meta model=...` for machine-readable context. - Use `--meta run_id=... --meta model=...` for machine-readable context.
- Never inline multiline content into the shell command; use a file or stdin. - Never inline multiline content into the shell command; use a file or stdin.
- Add `--dry-run` to see exactly what would be sent (method, headers, payload) without actually posting — useful when unsure a payload is well-formed. - Add `--dry-run` to see exactly what would be sent (method, headers, payload) without actually posting — useful when unsure a payload is well-formed.
## `curl` Fallback (No Helper Available)
Prefer the helper above — it handles multipart, content-type inference,
JSON shaping, and provenance flags for you. Use raw `curl` **only** when you
genuinely can't run the bundled Python script (e.g. a minimal environment
with no Python). Text artifacts post as JSON to `POST /api/artifacts` with
bearer-token auth:
```bash
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
-H "Authorization: Bearer $DAYBOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind":"markdown","title":"Build Report","source":"agent","content":"# Hello\n\nBody text."}'
```
File/image artifacts use multipart instead:
```bash
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
-H "Authorization: Bearer $DAYBOOK_TOKEN" \
-F "kind=image" -F "title=Architecture Diagram" -F "source=agent" \
-F "file=@diagram.png"
```
`GET $DAYBOOK_URL/api/capabilities` (no auth needed beyond a login/token per
that instance's config) lists supported kinds, methods, and the server
version if you need to confirm the contract before posting.

View file

@ -12,6 +12,7 @@ from django.utils import timezone
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from daybook_project import __version__
from artifacts.models import Artifact from artifacts.models import Artifact
User = get_user_model() User = get_user_model()
@ -78,6 +79,42 @@ class ArtifactAPITestCase(APITestCase):
response = self.client.get(reverse("capabilities-api")) response = self.client.get(reverse("capabilities-api"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_capabilities_advertises_skill(self):
response = self.client.get(reverse("capabilities-api"), **self.auth_headers(self.token))
self.assertEqual(response.data["methods"]["/api/skill"], ["GET"])
self.assertTrue(response.data["skill_url"].endswith("/api/skill"))
# -- skill ----------------------------------------------------------
def test_skill_requires_auth(self):
response = self.client.get(reverse("skill-api"))
self.assertEqual(response.status_code, 403)
def test_skill_returns_markdown_with_origin_filled_in(self):
response = self.client.get(reverse("skill-api"), **self.auth_headers(self.token))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/markdown; charset=utf-8")
body = response.content.decode()
# Frontmatter is intact and the config placeholder is rewritten to this
# instance's origin (http://testserver under the test client).
self.assertIn("name: daybook-posting", body)
self.assertIn("DAYBOOK_URL=http://testserver", body)
self.assertNotIn("your-daybook-instance.example.com", body)
def test_skill_json_format(self):
response = self.client.get(
reverse("skill-api"), {"format": "json"}, **self.auth_headers(self.token)
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["version"], __version__)
self.assertEqual(response.data["url"], "http://testserver")
self.assertIn("name: daybook-posting", response.data["skill"])
def test_skill_with_session(self):
self.client.force_login(self.user)
response = self.client.get(reverse("skill-api"))
self.assertEqual(response.status_code, 200)
# -- create (POST) -------------------------------------------------- # -- create (POST) --------------------------------------------------
def test_create_markdown_artifact(self): def test_create_markdown_artifact(self):