diff --git a/CHANGELOG.md b/CHANGELOG.md index 4281694..9ce888e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,16 +6,7 @@ the git tags (`vX.Y.Z`) and `daybook_project.__version__`. ## [Unreleased] -### 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. +_Nothing yet._ ## [0.1.0] — 2026-09-03 diff --git a/artifacts/urls.py b/artifacts/urls.py index dae96dc..98323e3 100644 --- a/artifacts/urls.py +++ b/artifacts/urls.py @@ -14,7 +14,6 @@ urlpatterns = [ path("assets/", views.asset_view, name="asset"), path("api/assets", views.AssetUploadView.as_view(), name="asset-upload-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.ArtifactDetailView.as_view(), name="artifact-detail-api"), path("settings/", views.settings_home, name="settings-home"), diff --git a/artifacts/views.py b/artifacts/views.py index a6ec867..555c121 100644 --- a/artifacts/views.py +++ b/artifacts/views.py @@ -322,57 +322,11 @@ class CapabilitiesView(APIView): "/api/artifacts": ["GET", "POST"], "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], "/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 diff --git a/docs/api.md b/docs/api.md index b3778c2..8d95c6f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -529,11 +529,8 @@ Example response: "methods": { "/api/artifacts": ["GET", "POST"], "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], - "/api/assets": ["POST"], - "/api/capabilities": ["GET"], - "/api/skill": ["GET"] - }, - "skill_url": "http://127.0.0.1:8001/api/skill" + "/api/assets": ["POST"] + } } ``` @@ -548,33 +545,6 @@ endpoints, it accepts a bearer token as well as a browser session: curl -H "Authorization: Bearer " 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 " http://127.0.0.1:8001/api/skill -``` - -Discover it programmatically via `skill_url` in the capabilities response. - ## Current MVP Constraints - Each user has a personal ingest API token; there is no shared token. diff --git a/skills/daybook-posting/SKILL.md b/skills/daybook-posting/SKILL.md index c972af9..51f4dc5 100644 --- a/skills/daybook-posting/SKILL.md +++ b/skills/daybook-posting/SKILL.md @@ -1,13 +1,26 @@ --- 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 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 @@ -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. ```text -DAYBOOK_URL=https://your-daybook-instance.example.com # your server's base URL -DAYBOOK_TOKEN=your-personal-api-token # from Settings -> Account -DAYBOOK_SOURCE=agent # a stable label for what's posting +DAYBOOK_URL=http://127.0.0.1:8001 +DAYBOOK_TOKEN=your-personal-api-token +DAYBOOK_SOURCE=agent ``` -- **`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.) +`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. ## Kind Cheat Sheet @@ -94,9 +65,8 @@ presentation. This script only **creates** artifacts — none of the following are 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 -`GET /api/capabilities`); enabling its public share link is feed-UI-only, -with no API equivalent at all yet. +or deleting one, requires the raw API (see `docs/api.md`); enabling its +public share link is feed-UI-only, with no API equivalent at all yet. ## CDN Snippets For `html_full` @@ -149,12 +119,11 @@ artifact), upload it first and reference the returned URL: ```bash # 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. -# The printed URL is absolute against your DAYBOOK_URL, e.g. -# ![chart]($DAYBOOK_URL/assets/ab12cd34ef56) -python "$DAYBOOK_POST" report.md +# 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. @@ -167,19 +136,19 @@ just post the file positionally; auto-mode detects `.csv`/`.json` and picks the right kind automatically: ```bash -python "$DAYBOOK_POST" results.csv -python "$DAYBOOK_POST" --kind json --title "Run Summary" --content-file summary.json +python skills/daybook-posting/scripts/daybook_post.py results.csv +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` (or post it positionally, same as any other file): ```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 -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. - 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): ```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. @@ -220,37 +189,8 @@ context like `run_id` or `model` — these end up in the artifact's ## Rules - 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 `--tags a,b,c` for filtering. - Use `--meta run_id=... --meta model=...` for machine-readable context. - 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. - -## `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. diff --git a/tests/api/test_artifacts.py b/tests/api/test_artifacts.py index f801988..967ed4f 100644 --- a/tests/api/test_artifacts.py +++ b/tests/api/test_artifacts.py @@ -12,7 +12,6 @@ from django.utils import timezone from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase -from daybook_project import __version__ from artifacts.models import Artifact User = get_user_model() @@ -79,42 +78,6 @@ class ArtifactAPITestCase(APITestCase): response = self.client.get(reverse("capabilities-api")) 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) -------------------------------------------------- def test_create_markdown_artifact(self):