Compare commits

..

No commits in common. "fb692612187f09bb32a5b36ea40e6553c5006b0e" and "6859be688e8f64ea207a7fe883281d1d61b95352" have entirely different histories.

6 changed files with 33 additions and 216 deletions

View file

@ -6,16 +6,7 @@ the git tags (`vX.Y.Z`) and `daybook_project.__version__`.
## [Unreleased] ## [Unreleased]
### Added _Nothing yet._
- `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,7 +14,6 @@ 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,57 +322,11 @@ 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,11 +529,8 @@ 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"
} }
``` ```
@ -548,33 +545,6 @@ 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,13 +1,26 @@
--- ---
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 a Daybook instance 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 Daybook 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.
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. Always use the helper script. Do not hand-build curl, JSON, PowerShell multipart, or inline shell content.
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
@ -17,54 +30,12 @@ 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=https://your-daybook-instance.example.com # your server's base URL DAYBOOK_URL=http://127.0.0.1:8001
DAYBOOK_TOKEN=your-personal-api-token # from Settings -> Account DAYBOOK_TOKEN=your-personal-api-token
DAYBOOK_SOURCE=agent # a stable label for what's posting DAYBOOK_SOURCE=agent
``` ```
- **`DAYBOOK_URL`** — the base URL of the Daybook instance you're posting to. `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.
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
@ -94,9 +65,8 @@ 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 the instance's `docs/api.md` or or deleting one, requires the raw API (see `docs/api.md`); enabling its
`GET /api/capabilities`); enabling its public share link is feed-UI-only, public share link is feed-UI-only, with no API equivalent at all yet.
with no API equivalent at all yet.
## CDN Snippets For `html_full` ## CDN Snippets For `html_full`
@ -149,12 +119,11 @@ 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 "$DAYBOOK_POST" --upload-image chart.png python skills/daybook-posting/scripts/daybook_post.py --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
# The printed URL is absolute against your DAYBOOK_URL, e.g. # ![chart](http://127.0.0.1:8001/assets/ab12cd34ef56)
# ![chart]($DAYBOOK_URL/assets/ab12cd34ef56) python skills/daybook-posting/scripts/daybook_post.py report.md
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.
@ -167,19 +136,19 @@ just post the file positionally; auto-mode detects `.csv`/`.json` and picks
the right kind automatically: the right kind automatically:
```bash ```bash
python "$DAYBOOK_POST" results.csv python skills/daybook-posting/scripts/daybook_post.py results.csv
python "$DAYBOOK_POST" --kind json --title "Run Summary" --content-file summary.json python skills/daybook-posting/scripts/daybook_post.py --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 "$DAYBOOK_POST" balance_sheet.xlsx python skills/daybook-posting/scripts/daybook_post.py 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 the instance's `docs/api.md` for the full list): limitations (see `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
@ -196,7 +165,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 "$DAYBOOK_POST" --file script.py --as-markdown --title "Generated Script" python skills/daybook-posting/scripts/daybook_post.py --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.
@ -220,37 +189,8 @@ 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,7 +12,6 @@ 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()
@ -79,42 +78,6 @@ 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):