v0.3.0: rename app Daybook -> AgentFeed

Removes the "Daybook" name from everything user- and agent-facing, since no
agents are configured yet and this is the cheap moment to do it.

- UI brand, docs, README, CLAUDE.md -> AgentFeed.
- Ingest env vars DAYBOOK_URL/TOKEN/SOURCE/DATA_DIR/BIND_ADDR/HOST_DATA_DIR/
  KEY_PROVISIONING_API -> AGENTFEED_* (token *values* unchanged).
- Skill: skills/daybook-posting -> skills/agentfeed-posting, daybook_post.py
  -> agentfeed_post.py (+ scripts/ wrapper and its test).
- static/daybook.css -> static/agentfeed.css; localStorage daybook.* ->
  agentfeed.*; templatetag daybook_extras -> agentfeed_extras; multipart
  boundaries; json/csv viewer element ids; daybook_exception_handler ->
  agentfeed_exception_handler; compose service/container -> agentfeed.
- Key-provisioning API now OFF by default (independent of DEBUG); enable with
  AGENTFEED_KEY_PROVISIONING_API=1 on test installs only.
- Posting helper now points at Settings -> API Keys.

Deferred on purpose (a later, separate step): the Django package
`daybook_project`, the `/opt/daybook` install path, and the Forgejo repo name.
The sibling AGENT_DAYBOOK__LLM_TEST name is intentionally untouched.

Full suite: 129 passing; live server verified serving under the new name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-04 13:10:28 +01:00
parent 4cacd8f64e
commit 8bc2215754
39 changed files with 303 additions and 284 deletions

View file

@ -8,6 +8,6 @@ Run the demo seeding script to populate today with one tagged sample artifact pe
python scripts/seed_demo_artifacts.py python scripts/seed_demo_artifacts.py
``` ```
Requires the Daybook dev server to be running on the project's fixed local port: `python manage.py runserver 127.0.0.1:8001`. The script defaults to `DAYBOOK_URL=http://127.0.0.1:8001` / `DAYBOOK_TOKEN=dev-token`; no overrides needed unless the user has changed these. Requires the AgentFeed dev server to be running on the project's fixed local port: `python manage.py runserver 127.0.0.1:8001`. The script defaults to `AGENTFEED_URL=http://127.0.0.1:8001` / `AGENTFEED_TOKEN=dev-token`; no overrides needed unless the user has changed these.
After running, report each posted artifact's kind and URL, and remind the user to reload the feed for today. After running, report each posted artifact's kind and URL, and remind the user to reload the feed for today.

View file

@ -1,13 +1,32 @@
# Changelog # Changelog
All notable changes to Daybook are recorded here. Format loosely follows All notable changes to AgentFeed are recorded here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/); versions are SemVer and match [Keep a Changelog](https://keepachangelog.com/); versions are SemVer and match
the git tags (`vX.Y.Z`) and `daybook_project.__version__`. the git tags (`vX.Y.Z`) and `__version__`. (Entries before 0.3.0 use the app's
former name, "Daybook".)
## [Unreleased] ## [Unreleased]
_Nothing yet._ _Nothing yet._
## [0.3.0] — 2026-09-04
### Changed
- **Renamed the app to AgentFeed.** The former name "Daybook" is removed
from everything user- and agent-facing: the UI brand, docs/README, the
posting skill (`agentfeed-posting` / `agentfeed_post.py`), CSS
(`agentfeed.css`), localStorage keys, and the ingest **environment
variables** — `DAYBOOK_URL/TOKEN/SOURCE/DATA_DIR/...` are now
`AGENTFEED_*`. Agents must update their env-var names (the token *values*
are unchanged). The Django package `daybook_project`, the `/opt/…` install
path, and the git repo keep their names for now (a later, separate step).
- Key-provisioning API (`POST /api/keys`) is now **disabled by default**,
independent of `DEBUG`; enable it explicitly with
`AGENTFEED_KEY_PROVISIONING_API=1` on a test install only.
### Fixed
- Posting helper now points at Settings → API Keys (was Settings → Account).
## [0.2.1] — 2026-09-04 ## [0.2.1] — 2026-09-04
### Added ### Added
@ -98,7 +117,8 @@ First tagged release.
- Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a - Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a
runbook (`deploy/cloud_agent_deploy.md`) and health check. runbook (`deploy/cloud_agent_deploy.md`) and health check.
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.2.1...HEAD [Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.3.0...HEAD
[0.3.0]: https://git.cygnus.systems/peter/daybook/compare/v0.2.1...v0.3.0
[0.2.1]: https://git.cygnus.systems/peter/daybook/compare/v0.2.0...v0.2.1 [0.2.1]: https://git.cygnus.systems/peter/daybook/compare/v0.2.0...v0.2.1
[0.2.0]: https://git.cygnus.systems/peter/daybook/compare/v0.1.1...v0.2.0 [0.2.0]: https://git.cygnus.systems/peter/daybook/compare/v0.1.1...v0.2.0
[0.1.1]: https://git.cygnus.systems/peter/daybook/compare/v0.1.0...v0.1.1 [0.1.1]: https://git.cygnus.systems/peter/daybook/compare/v0.1.0...v0.1.1

View file

@ -2,7 +2,7 @@
## Project ## Project
Daybook is a self-hosted, multi-user Django + Django REST Framework web app where AI agents, scripts, or people publish artifacts and each logged-in user browses their own time-ordered, label-filterable feed. AgentFeed is a self-hosted, multi-user Django + Django REST Framework web app where AI agents, scripts, or people publish artifacts and each logged-in user browses their own time-ordered, label-filterable feed.
The core product principle is: agents publish content, not files. The server owns IDs, styling, storage layout, dates, and normalization into browser-native HTML, image, or PDF outputs. The core product principle is: agents publish content, not files. The server owns IDs, styling, storage layout, dates, and normalization into browser-native HTML, image, or PDF outputs.
@ -44,7 +44,7 @@ The core product principle is: agents publish content, not files. The server own
- Use the local Django commands for normal iteration: `python manage.py check`, `python manage.py migrate`, and `python manage.py runserver`. - Use the local Django commands for normal iteration: `python manage.py check`, `python manage.py migrate`, and `python manage.py runserver`.
- Run the local development server on `127.0.0.1:8001`. Port `8000` may be reserved or blocked on this workstation. - Run the local development server on `127.0.0.1:8001`. Port `8000` may be reserved or blocked on this workstation.
- Always use this same fixed port for the dev server. Never start a second instance on a different port; if `8001` is already serving, reuse it rather than starting another. - Always use this same fixed port for the dev server. Never start a second instance on a different port; if `8001` is already serving, reuse it rather than starting another.
- All artifact-posting tools (`skills/daybook-posting/scripts/daybook_post.py`, `scripts/seed_demo_artifacts.py`, and any future posting skill) must default `DAYBOOK_URL` to `http://127.0.0.1:8001` to match. - All artifact-posting tools (`skills/agentfeed-posting/scripts/agentfeed_post.py`, `scripts/seed_demo_artifacts.py`, and any future posting skill) must default `AGENTFEED_URL` to `http://127.0.0.1:8001` to match.
- A regular (non-superuser) user `test` / password `testpwd` must always exist in the local dev database — it's the permanent account a sibling project (`AGENT_DAYBOOK__LLM_TEST`) uses for API testing. If the local `db.sqlite3` is ever reset, recreate this exact user (e.g. via Settings → Users) before assuming that project is broken. - A regular (non-superuser) user `test` / password `testpwd` must always exist in the local dev database — it's the permanent account a sibling project (`AGENT_DAYBOOK__LLM_TEST`) uses for API testing. If the local `db.sqlite3` is ever reset, recreate this exact user (e.g. via Settings → Users) before assuming that project is broken.
- Do not deploy to the Proxmox NUC during routine code iteration. - Do not deploy to the Proxmox NUC during routine code iteration.
- Treat the NUC container as the local live/staging target. Deploy there only at explicit stable checkpoints, such as an end-of-day build. - Treat the NUC container as the local live/staging target. Deploy there only at explicit stable checkpoints, such as an end-of-day build.

View file

@ -1,6 +1,6 @@
# Daybook # AgentFeed
Daybook is a self-hosted, multi-user **artifact journal**. AI agents, scripts, AgentFeed is a self-hosted, multi-user **artifact journal**. AI agents, scripts,
or people publish finished outputs — reports, HTML, images, PDFs, decks, JSON, or people publish finished outputs — reports, HTML, images, PDFs, decks, JSON,
CSV — and each logged-in user browses their own time-ordered, label-filterable CSV — and each logged-in user browses their own time-ordered, label-filterable
feed. The core principle: **agents publish content, not files.** The server feed. The core principle: **agents publish content, not files.** The server
@ -35,9 +35,9 @@ Then open <http://127.0.0.1:8001/> and log in.
|---|---| |---|---|
| [`docs/agent_quickstart.md`](docs/agent_quickstart.md) | Shortest path to posting from an agent | | [`docs/agent_quickstart.md`](docs/agent_quickstart.md) | Shortest path to posting from an agent |
| [`docs/api.md`](docs/api.md) | Full REST API reference (kinds, endpoints, keys, sharing) | | [`docs/api.md`](docs/api.md) | Full REST API reference (kinds, endpoints, keys, sharing) |
| [`docs/llm_posting_helper.md`](docs/llm_posting_helper.md) | Using the bundled `daybook_post.py` helper | | [`docs/llm_posting_helper.md`](docs/llm_posting_helper.md) | Using the bundled `agentfeed_post.py` helper |
| [`docs/deployment.md`](docs/deployment.md) | Docker deployment, reverse proxy, `/admin/`, backup | | [`docs/deployment.md`](docs/deployment.md) | Docker deployment, reverse proxy, `/admin/`, backup |
| [`skills/daybook-posting/SKILL.md`](skills/daybook-posting/SKILL.md) | The agent-posting skill (also served at `/api/skill`) | | [`skills/agentfeed-posting/SKILL.md`](skills/agentfeed-posting/SKILL.md) | The agent-posting skill (also served at `/api/skill`) |
| [`CHANGELOG.md`](CHANGELOG.md) | Release history | | [`CHANGELOG.md`](CHANGELOG.md) | Release history |
## Admin ## Admin

View file

@ -2,7 +2,7 @@ from rest_framework.response import Response
from rest_framework.views import exception_handler from rest_framework.views import exception_handler
def daybook_exception_handler(exc, context): def agentfeed_exception_handler(exc, context):
"""DRF exception handler that additionally maps pathological-input crashes """DRF exception handler that additionally maps pathological-input crashes
to a clean 400 instead of an unhandled 500. to a clean 400 instead of an unhandled 500.

View file

@ -2,5 +2,5 @@ from django import forms
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
class DaybookLoginForm(AuthenticationForm): class AgentFeedLoginForm(AuthenticationForm):
remember_me = forms.BooleanField(required=False, initial=True) remember_me = forms.BooleanField(required=False, initial=True)

View file

@ -5,7 +5,7 @@ from django.db import models
def generate_api_key(): def generate_api_key():
"""`dbk_` prefix (recognizable as a Daybook key) + 40 hex chars.""" """`dbk_` prefix (recognizable as a AgentFeed key) + 40 hex chars."""
return "dbk_" + secrets.token_hex(20) return "dbk_" + secrets.token_hex(20)

View file

@ -34,7 +34,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from .demo_data import create_demo_artifacts from .demo_data import create_demo_artifacts
from .forms import DaybookLoginForm from .forms import AgentFeedLoginForm
from .models import ApiKey, Artifact, Asset, TagColor from .models import ApiKey, Artifact, Asset, TagColor
from .storage import ( from .storage import (
QuotaExceeded, QuotaExceeded,
@ -66,7 +66,7 @@ REMEMBER_ME_SESSION_AGE = 60 * 60 * 24 * 30 # 30 days
MAX_ASSET_BYTES = 50 * 1024 * 1024 # 50 MB per uploaded image MAX_ASSET_BYTES = 50 * 1024 * 1024 # 50 MB per uploaded image
# Matches the asset id in a Daybook asset URL, e.g. /assets/ab12cd34ef56 # Matches the asset id in a AgentFeed asset URL, e.g. /assets/ab12cd34ef56
ASSET_URL_RE = re.compile(r"/assets/([0-9a-f]{12})") ASSET_URL_RE = re.compile(r"/assets/([0-9a-f]{12})")
@ -315,7 +315,7 @@ class CapabilitiesView(APIView):
"/api/capabilities": ["GET"], "/api/capabilities": ["GET"],
"/api/skill": ["GET"], "/api/skill": ["GET"],
} }
if settings.DAYBOOK_KEY_PROVISIONING_API: if settings.AGENTFEED_KEY_PROVISIONING_API:
methods["/api/keys"] = ["GET", "POST"] methods["/api/keys"] = ["GET", "POST"]
methods["/api/keys/{id}"] = ["DELETE"] methods["/api/keys/{id}"] = ["DELETE"]
@ -330,30 +330,30 @@ class CapabilitiesView(APIView):
"auth": "bearer_token", "auth": "bearer_token",
"methods": methods, "methods": methods,
"skill_url": request.build_absolute_uri(reverse("skill-api")), "skill_url": request.build_absolute_uri(reverse("skill-api")),
"key_provisioning_api": settings.DAYBOOK_KEY_PROVISIONING_API, "key_provisioning_api": settings.AGENTFEED_KEY_PROVISIONING_API,
} }
) )
SKILL_MD_PATH = settings.BASE_DIR / "skills" / "daybook-posting" / "SKILL.md" SKILL_MD_PATH = settings.BASE_DIR / "skills" / "agentfeed-posting" / "SKILL.md"
def render_posting_skill(origin): def render_posting_skill(origin):
"""Return the bundled posting SKILL.md with its DAYBOOK_URL placeholder """Return the bundled posting SKILL.md with its AGENTFEED_URL placeholder
rewritten to this instance's origin, so text handed to an agent already 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; points at the right server. Only the config-block assignment is touched;
the surrounding prose (which explains DAYBOOK_URL) is left alone.""" the surrounding prose (which explains AGENTFEED_URL) is left alone."""
text = SKILL_MD_PATH.read_text(encoding="utf-8") text = SKILL_MD_PATH.read_text(encoding="utf-8")
return re.sub( return re.sub(
r"(?m)^(DAYBOOK_URL=)\S+", r"(?m)^(AGENTFEED_URL=)\S+",
lambda m: m.group(1) + origin, lambda m: m.group(1) + origin,
text, text,
) )
class SkillView(APIView): class SkillView(APIView):
"""Serve the bundled agent-posting skill (skills/daybook-posting/SKILL.md), """Serve the bundled agent-posting skill (skills/agentfeed-posting/SKILL.md),
with DAYBOOK_URL pre-filled to this instance so a recipient posts to the with AGENTFEED_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 right server. Same auth as capabilities: a logged-in user can copy it, or
an agent can fetch it with its API token. an agent can fetch it with its API token.
@ -390,7 +390,7 @@ def serialize_api_key(api_key):
class ApiKeyListCreateView(APIView): class ApiKeyListCreateView(APIView):
"""Programmatic API-key management, for automated test installations that """Programmatic API-key management, for automated test installations that
provision their own agents. Gated by settings.DAYBOOK_KEY_PROVISIONING_API, provision their own agents. Gated by settings.AGENTFEED_KEY_PROVISIONING_API,
which is OFF in deployment (it hands out key material over the API). When which is OFF in deployment (it hands out key material over the API). When
off, every method returns 403. Authenticated by session or an existing off, every method returns 403. Authenticated by session or an existing
bearer token, and only ever operates on that user's own keys. bearer token, and only ever operates on that user's own keys.
@ -405,7 +405,7 @@ class ApiKeyListCreateView(APIView):
return resolve_token_user(request) return resolve_token_user(request)
def get(self, request): def get(self, request):
if not settings.DAYBOOK_KEY_PROVISIONING_API: if not settings.AGENTFEED_KEY_PROVISIONING_API:
return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403) return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403)
user = self._resolve_user(request) user = self._resolve_user(request)
if user is None: if user is None:
@ -413,7 +413,7 @@ class ApiKeyListCreateView(APIView):
return Response([serialize_api_key(k) for k in user.api_keys.all()]) return Response([serialize_api_key(k) for k in user.api_keys.all()])
def post(self, request): def post(self, request):
if not settings.DAYBOOK_KEY_PROVISIONING_API: if not settings.AGENTFEED_KEY_PROVISIONING_API:
return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403) return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403)
user = self._resolve_user(request) user = self._resolve_user(request)
if user is None: if user is None:
@ -432,7 +432,7 @@ class ApiKeyDetailView(APIView):
""" """
def delete(self, request, key_id): def delete(self, request, key_id):
if not settings.DAYBOOK_KEY_PROVISIONING_API: if not settings.AGENTFEED_KEY_PROVISIONING_API:
return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403) return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403)
user = request.user if request.user.is_authenticated else resolve_token_user(request) user = request.user if request.user.is_authenticated else resolve_token_user(request)
if user is None: if user is None:
@ -858,7 +858,7 @@ def create_binary_artifact(data, request, user):
# Allowlist for sanitizing the "themed" text kinds (markdown, deck, # Allowlist for sanitizing the "themed" text kinds (markdown, deck,
# html_fragment) — kinds where Daybook, not the poster, owns presentation, so # html_fragment) — kinds where AgentFeed, not the poster, owns presentation, so
# they must never be a script-execution vector (issue #014). html_full is the # they must never be a script-execution vector (issue #014). html_full is the
# deliberate exception (verbatim, poster owns everything) and is NOT run # deliberate exception (verbatim, poster owns everything) and is NOT run
# through this. The list keeps ordinary formatting + tables + code-highlight # through this. The list keeps ordinary formatting + tables + code-highlight
@ -963,14 +963,14 @@ def wrap_json_viewer(content, title, request):
</style> </style>
</head> </head>
<body class="artifact"> <body class="artifact">
<script type="application/json" id="daybook-data">{safe_json}</script> <script type="application/json" id="agentfeed-data">{safe_json}</script>
<div id="daybook-json-view"></div> <div id="agentfeed-json-view"></div>
<script> <script>
(function () {{ (function () {{
var raw = document.getElementById("daybook-data").textContent; var raw = document.getElementById("agentfeed-data").textContent;
var data = JSON.parse(raw); var data = JSON.parse(raw);
renderjson.set_show_to_level(2); renderjson.set_show_to_level(2);
document.getElementById("daybook-json-view").appendChild(renderjson(data)); document.getElementById("agentfeed-json-view").appendChild(renderjson(data));
}})(); }})();
</script> </script>
</body> </body>
@ -1007,18 +1007,18 @@ def wrap_csv_viewer(content, title, request):
</style> </style>
</head> </head>
<body class="artifact"> <body class="artifact">
<script type="application/json" id="daybook-data">{table_json}</script> <script type="application/json" id="agentfeed-data">{table_json}</script>
<div id="daybook-csv-view"></div> <div id="agentfeed-csv-view"></div>
<script> <script>
(function () {{ (function () {{
var table = JSON.parse(document.getElementById("daybook-data").textContent); var table = JSON.parse(document.getElementById("agentfeed-data").textContent);
new gridjs.Grid({{ new gridjs.Grid({{
columns: table.columns, columns: table.columns,
data: table.data, data: table.data,
search: true, search: true,
sort: true, sort: true,
pagination: {{ limit: 20 }}, pagination: {{ limit: 20 }},
}}).render(document.getElementById("daybook-csv-view")); }}).render(document.getElementById("agentfeed-csv-view"));
}})(); }})();
</script> </script>
</body> </body>
@ -1306,9 +1306,9 @@ def raw_artifact_path(artifact):
return None return None
class DaybookLoginView(auth_views.LoginView): class AgentFeedLoginView(auth_views.LoginView):
template_name = "artifacts/login.html" template_name = "artifacts/login.html"
authentication_form = DaybookLoginForm authentication_form = AgentFeedLoginForm
def form_valid(self, form): def form_valid(self, form):
response = super().form_valid(form) response = super().form_valid(form)
@ -1459,7 +1459,7 @@ def raw_unavailable_html(artifact):
<body> <body>
<main> <main>
<h1>Raw source unavailable</h1> <h1>Raw source unavailable</h1>
<p>Daybook does not have preserved raw source for <strong>{html.escape(artifact.title)}</strong>.</p> <p>AgentFeed does not have preserved raw source for <strong>{html.escape(artifact.title)}</strong>.</p>
</main> </main>
</body> </body>
</html> </html>

View file

@ -1,4 +1,4 @@
# Single source of truth for the app version. Bump this, commit, then tag the # Single source of truth for the app version. Bump this, commit, then tag the
# commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in # commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in
# GET /api/capabilities ("version"). # GET /api/capabilities ("version").
__version__ = "0.2.1" __version__ = "0.3.0"

View file

@ -3,7 +3,7 @@ from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = Path(os.environ.get("DAYBOOK_DATA_DIR", BASE_DIR)).resolve() DATA_DIR = Path(os.environ.get("AGENTFEED_DATA_DIR", BASE_DIR)).resolve()
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-change-me") SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-change-me")
DEBUG = os.environ.get("DJANGO_DEBUG", "1").lower() in {"1", "true", "yes", "on"} DEBUG = os.environ.get("DJANGO_DEBUG", "1").lower() in {"1", "true", "yes", "on"}
@ -11,14 +11,13 @@ ALLOWED_HOSTS = [host.strip() for host in os.environ.get("DJANGO_ALLOWED_HOSTS",
# Allow creating/listing/revoking API keys over the ingest API (POST /api/keys). # Allow creating/listing/revoking API keys over the ingest API (POST /api/keys).
# Meant for automated test installations that provision their own agents. It # Meant for automated test installations that provision their own agents. It
# exposes key material over the API, so it is OFF in deployment. Unset, it # exposes key material over the API, so it is DISABLED BY DEFAULT and must be
# follows DEBUG (on locally, off in production); set DAYBOOK_KEY_PROVISIONING_API # turned on explicitly — set AGENTFEED_KEY_PROVISIONING_API=1 only on a throwaway
# explicitly (0/1) to override either way. # test install. Never enable it in a real deployment. (Independent of DEBUG, so
_key_api_env = os.environ.get("DAYBOOK_KEY_PROVISIONING_API") # a misconfigured DEBUG=1 production box still won't expose it.)
if _key_api_env is None: AGENTFEED_KEY_PROVISIONING_API = os.environ.get(
DAYBOOK_KEY_PROVISIONING_API = DEBUG "AGENTFEED_KEY_PROVISIONING_API", ""
else: ).lower() in {"1", "true", "yes", "on"}
DAYBOOK_KEY_PROVISIONING_API = _key_api_env.lower() in {"1", "true", "yes", "on"}
# Needed when running behind a TLS-terminating reverse proxy (Apache/Nginx/etc.): # Needed when running behind a TLS-terminating reverse proxy (Apache/Nginx/etc.):
# Django otherwise thinks every request is plain HTTP, which breaks CSRF checks # Django otherwise thinks every request is plain HTTP, which breaks CSRF checks
@ -71,7 +70,7 @@ WSGI_APPLICATION = "daybook_project.wsgi.application"
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Maps deeply-nested-JSON RecursionErrors (raised in DRF's parser, before # Maps deeply-nested-JSON RecursionErrors (raised in DRF's parser, before
# view validation) to a 400 instead of an unhandled 500. See issue #015. # view validation) to a 400 instead of an unhandled 500. See issue #015.
"EXCEPTION_HANDLER": "artifacts.exception_handlers.daybook_exception_handler", "EXCEPTION_HANDLER": "artifacts.exception_handlers.agentfeed_exception_handler",
} }
DATABASES = { DATABASES = {

View file

@ -2,12 +2,12 @@ from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import include, path from django.urls import include, path
from artifacts.views import DaybookLoginView from artifacts.views import AgentFeedLoginView
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("accounts/login/", DaybookLoginView.as_view(), name="login"), path("accounts/login/", AgentFeedLoginView.as_view(), name="login"),
path("accounts/logout/", auth_views.LogoutView.as_view(next_page="login"), name="logout"), path("accounts/logout/", auth_views.LogoutView.as_view(next_page="login"), name="logout"),
path("", include("artifacts.urls")), path("", include("artifacts.urls")),
] ]

View file

@ -1,7 +1,7 @@
# Cloud Deployment Runbook For A Dedicated Deploy Agent # Cloud Deployment Runbook For A Dedicated Deploy Agent
This is written for a capable (Opus-class) agent deploying and operating This is written for a capable (Opus-class) agent deploying and operating
Daybook on a cloud VM. Treat it as a guide and a source of Daybook-specific AgentFeed on a cloud VM. Treat it as a guide and a source of AgentFeed-specific
facts, **not a rigid script** — you're expected to use judgment, adapt to facts, **not a rigid script** — you're expected to use judgment, adapt to
the actual VM, and read the authoritative sources rather than follow steps the actual VM, and read the authoritative sources rather than follow steps
blindly. The steps below are a sensible default path; deviate where the host blindly. The steps below are a sensible default path; deviate where the host
@ -25,7 +25,7 @@ disagree):
Where a decision needs info you don't have (VM IP, domain, whether it's Where a decision needs info you don't have (VM IP, domain, whether it's
internet-facing, whether a reverse proxy/secondary volume exists), ask the internet-facing, whether a reverse proxy/secondary volume exists), ask the
operator once, then proceed autonomously for routine redeploys. The rest of operator once, then proceed autonomously for routine redeploys. The rest of
this file is the recommended path plus the non-obvious, Daybook-specific this file is the recommended path plus the non-obvious, AgentFeed-specific
gotchas (SSH port, CSRF-behind-proxy, bind address, data volume, healthcheck) gotchas (SSH port, CSRF-behind-proxy, bind address, data volume, healthcheck)
that are easy to get wrong. that are easy to get wrong.
@ -37,9 +37,9 @@ a token tied to a full user account.
1. Generate an SSH key pair on the VM (or ask the operator for one to install): 1. Generate an SSH key pair on the VM (or ask the operator for one to install):
```bash ```bash
ssh-keygen -t ed25519 -C "daybook-deploy-agent" -f ~/.ssh/daybook_deploy -N "" ssh-keygen -t ed25519 -C "agentfeed-deploy-agent" -f ~/.ssh/agentfeed_deploy -N ""
``` ```
2. Ask the operator to add the **public** key (`~/.ssh/daybook_deploy.pub`) as 2. Ask the operator to add the **public** key (`~/.ssh/agentfeed_deploy.pub`) as
a Deploy Key on the repo in Forgejo (repo → Settings → Deploy Keys → a Deploy Key on the repo in Forgejo (repo → Settings → Deploy Keys →
read-only is sufficient), or hand it to them directly — never ask them to read-only is sufficient), or hand it to them directly — never ask them to
give you their personal account credentials. give you their personal account credentials.
@ -52,7 +52,7 @@ a token tied to a full user account.
cat >> ~/.ssh/config <<'EOF' cat >> ~/.ssh/config <<'EOF'
Host git.cygnus.systems Host git.cygnus.systems
Port 2222 Port 2222
IdentityFile ~/.ssh/daybook_deploy IdentityFile ~/.ssh/agentfeed_deploy
IdentitiesOnly yes IdentitiesOnly yes
EOF EOF
``` ```
@ -112,12 +112,12 @@ Edit `deploy/.env`:
DJANGO_DEBUG=0 DJANGO_DEBUG=0
DJANGO_SECRET_KEY=<generated-secret> DJANGO_SECRET_KEY=<generated-secret>
DJANGO_ALLOWED_HOSTS=<vm-public-ip>,<domain-if-any>,localhost,127.0.0.1 DJANGO_ALLOWED_HOSTS=<vm-public-ip>,<domain-if-any>,localhost,127.0.0.1
DAYBOOK_DATA_DIR=/data AGENTFEED_DATA_DIR=/data
DATABASE_PATH=/data/db.sqlite3 DATABASE_PATH=/data/db.sqlite3
ARTIFACTS_ROOT=/data/artifacts_store ARTIFACTS_ROOT=/data/artifacts_store
``` ```
There is no shared API token to set — Daybook issues a personal token per There is no shared API token to set — AgentFeed issues a personal token per
user from Settings → Account once a user exists (step 5). Never commit user from Settings → Account once a user exists (step 5). Never commit
`deploy/.env`; it is already gitignored. `deploy/.env`; it is already gitignored.
@ -131,17 +131,17 @@ if unsure which applies here.
## 4. Network Access ## 4. Network Access
**If this VM is reachable from the public internet, a reverse proxy with TLS **If this VM is reachable from the public internet, a reverse proxy with TLS
in front of Daybook is not optional — it's the only path.** Bearer tokens are in front of AgentFeed is not optional — it's the only path.** Bearer tokens are
this app's entire write-auth model, and they'd otherwise go over plaintext this app's entire write-auth model, and they'd otherwise go over plaintext
HTTP straight to a port on the public IP. Direct port exposure (below) is HTTP straight to a port on the public IP. Direct port exposure (below) is
only for private/VPN-only networks with no public exposure. only for private/VPN-only networks with no public exposure.
- **Internet-facing (the common case):** set `DAYBOOK_BIND_ADDR=127.0.0.1` in - **Internet-facing (the common case):** set `AGENTFEED_BIND_ADDR=127.0.0.1` in
`deploy/.env` so the container port is not reachable from outside the VM at `deploy/.env` so the container port is not reachable from outside the VM at
all, then open `80`/`443` in the VM's firewall/security group for the all, then open `80`/`443` in the VM's firewall/security group for the
reverse proxy (see §8). Do not open `8000` externally. reverse proxy (see §8). Do not open `8000` externally.
- **Private/VPN-only network:** open TCP `8000` to the addresses that should - **Private/VPN-only network:** open TCP `8000` to the addresses that should
reach it (never `0.0.0.0/0`), and leave `DAYBOOK_BIND_ADDR` unset. reach it (never `0.0.0.0/0`), and leave `AGENTFEED_BIND_ADDR` unset.
Before opening any port, check whether one is already open and in use — Before opening any port, check whether one is already open and in use —
`ss -tlnp | grep -E ':80|:443|:8000'` — rather than assuming a clean slate. `ss -tlnp | grep -E ':80|:443|:8000'` — rather than assuming a clean slate.
@ -153,8 +153,8 @@ don't have API access to it yourself.
## 5. Build, Start, Create A User ## 5. Build, Start, Create A User
If the target has a separate, larger data volume than its main disk (common If the target has a separate, larger data volume than its main disk (common
on space-constrained VMs), point Daybook's persistent data at it before on space-constrained VMs), point AgentFeed's persistent data at it before
starting: set `DAYBOOK_HOST_DATA_DIR=/path/on/that/volume` in `deploy/.env`. starting: set `AGENTFEED_HOST_DATA_DIR=/path/on/that/volume` in `deploy/.env`.
Ask the operator whether such a volume exists and where before defaulting to Ask the operator whether such a volume exists and where before defaulting to
the main disk — filling the main disk on a host running other services is a the main disk — filling the main disk on a host running other services is a
bigger problem than it would be on a dedicated box. Create that same path bigger problem than it would be on a dedicated box. Create that same path
@ -162,9 +162,9 @@ bigger problem than it would be on a dedicated box. Create that same path
unset) before starting the stack: unset) before starting the stack:
```bash ```bash
mkdir -p deploy/data # or the DAYBOOK_HOST_DATA_DIR path if you set one above mkdir -p deploy/data # or the AGENTFEED_HOST_DATA_DIR path if you set one above
docker compose -f deploy/docker-compose.yml up -d --build docker compose -f deploy/docker-compose.yml up -d --build
docker compose -f deploy/docker-compose.yml exec daybook python manage.py createsuperuser docker compose -f deploy/docker-compose.yml exec agentfeed python manage.py createsuperuser
``` ```
The entrypoint runs migrations and collects static files automatically on The entrypoint runs migrations and collects static files automatically on
@ -183,7 +183,7 @@ response when unauthenticated — it confirms the app is up. (Don't use
container's own healthcheck hits the login page instead, which is 200 container's own healthcheck hits the login page instead, which is 200
without auth.) This `curl` runs without auth.) This `curl` runs
against `127.0.0.1` from on the VM itself, so it works regardless of whether against `127.0.0.1` from on the VM itself, so it works regardless of whether
`DAYBOOK_BIND_ADDR` is set. Log in from a browser at whichever address is `AGENTFEED_BIND_ADDR` is set. Log in from a browser at whichever address is
actually externally reachable at this point in the runbook — `http://<vm-ip>:8000/` actually externally reachable at this point in the runbook — `http://<vm-ip>:8000/`
if you're on the private/VPN-only path from §4, or `https://<domain>/` through if you're on the private/VPN-only path from §4, or `https://<domain>/` through
the reverse proxy once §8 is done if you're on the internet-facing path — with the reverse proxy once §8 is done if you're on the internet-facing path — with
@ -230,13 +230,13 @@ For a routine forward release that's fine; if a rollback is needed after a
migration, restore the pre-deploy `deploy/data` backup (see §9) alongside migration, restore the pre-deploy `deploy/data` backup (see §9) alongside
the code checkout. the code checkout.
`deploy/.env` and the data directory (wherever `DAYBOOK_HOST_DATA_DIR` points, `deploy/.env` and the data directory (wherever `AGENTFEED_HOST_DATA_DIR` points,
or `deploy/data` by default) are untracked, so `git pull` never touches them — or `deploy/data` by default) are untracked, so `git pull` never touches them —
no need to back them up before a routine redeploy. no need to back them up before a routine redeploy.
**If the container crash-loops right after a build** (`docker compose ps` **If the container crash-loops right after a build** (`docker compose ps`
shows `restarting`, health never goes green), the most likely cause is the shows `restarting`, health never goes green), the most likely cause is the
entrypoint's `collectstatic` step failing — check `docker logs daybook` entrypoint's `collectstatic` step failing — check `docker logs agentfeed`
before assuming an env/network problem. A `whitenoise ... MissingFileError` before assuming an env/network problem. A `whitenoise ... MissingFileError`
there means a static asset references a file that isn't present (this is what there means a static asset references a file that isn't present (this is what
bit v0.1.0). No data or schema impact; fix the asset in the repo, tag, and bit v0.1.0). No data or schema impact; fix the asset in the repo, tag, and
@ -277,14 +277,14 @@ instead. Apache example:
```apache ```apache
<VirtualHost *:443> <VirtualHost *:443>
ServerName daybook.example.com ServerName agentfeed.example.com
ProxyPreserveHost On ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https" RequestHeader set X-Forwarded-Proto "https"
ProxyPass / http://127.0.0.1:8000/ ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/ ProxyPassReverse / http://127.0.0.1:8000/
SSLEngine on SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/daybook.example.com/fullchain.pem SSLCertificateFile /etc/letsencrypt/live/agentfeed.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/daybook.example.com/privkey.pem SSLCertificateKeyFile /etc/letsencrypt/live/agentfeed.example.com/privkey.pem
</VirtualHost> </VirtualHost>
``` ```
@ -296,7 +296,7 @@ Only reach for a standalone proxy like Caddy if the target genuinely has
nothing on 80/443 yet — it still needs the same header: nothing on 80/443 yet — it still needs the same header:
```text ```text
daybook.example.com { agentfeed.example.com {
reverse_proxy 127.0.0.1:8000 { reverse_proxy 127.0.0.1:8000 {
header_up X-Forwarded-Proto https header_up X-Forwarded-Proto https
} }
@ -305,15 +305,15 @@ daybook.example.com {
## 9. Backup ## 9. Backup
If you set `DAYBOOK_HOST_DATA_DIR` in step 5, the real data lives there, not If you set `AGENTFEED_HOST_DATA_DIR` in step 5, the real data lives there, not
at `deploy/data` — back up whichever path is actually configured, read at `deploy/data` — back up whichever path is actually configured, read
straight from `deploy/.env` rather than assumed, so this keeps working straight from `deploy/.env` rather than assumed, so this keeps working
whether or not that override is set: whether or not that override is set:
```bash ```bash
cd /opt/daybook cd /opt/daybook
DATA_DIR="$(grep -oP '(?<=^DAYBOOK_HOST_DATA_DIR=).*' deploy/.env || echo deploy/data)" DATA_DIR="$(grep -oP '(?<=^AGENTFEED_HOST_DATA_DIR=).*' deploy/.env || echo deploy/data)"
tar -czf "/tmp/daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")" tar -czf "/tmp/agentfeed-data-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
``` ```
Copy the resulting archive off the VM (to object storage, the operator's Copy the resulting archive off the VM (to object storage, the operator's
@ -331,12 +331,12 @@ Before reporting the deployment complete, confirm all of these:
public HTTPS origin, and the proxy forwards `X-Forwarded-Proto: https`. public HTTPS origin, and the proxy forwards `X-Forwarded-Proto: https`.
Verify by actually logging in through the proxy, not just curling Verify by actually logging in through the proxy, not just curling
`/api/capabilities` — CSRF only bites on POST (login, artifact writes). `/api/capabilities` — CSRF only bites on POST (login, artifact writes).
- [ ] `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy. - [ ] `docker compose -f deploy/docker-compose.yml ps` shows `agentfeed` healthy.
- [ ] `/api/capabilities` returns `403` unauthenticated, `200` with a token. - [ ] `/api/capabilities` returns `403` unauthenticated, `200` with a token.
- [ ] At least one superuser exists with a working personal API token. - [ ] At least one superuser exists with a working personal API token.
- [ ] A smoke-test artifact posts and renders successfully. - [ ] A smoke-test artifact posts and renders successfully.
- [ ] If internet-facing: TLS is in front of the app, not raw HTTP on `8000`. - [ ] If internet-facing: TLS is in front of the app, not raw HTTP on `8000`.
- [ ] A backup of the data directory (`deploy/data`, or `DAYBOOK_HOST_DATA_DIR` - [ ] A backup of the data directory (`deploy/data`, or `AGENTFEED_HOST_DATA_DIR`
if you set it) has been taken and copied off the VM. if you set it) has been taken and copied off the VM.
- [ ] If the target host has its own service-inventory, monitoring, or - [ ] If the target host has its own service-inventory, monitoring, or
changelog conventions for new services, they've been followed — ask changelog conventions for new services, they've been followed — ask

View file

@ -1,4 +1,4 @@
# ${VAR} substitution below (DAYBOOK_BIND_ADDR, DAYBOOK_HOST_DATA_DIR) reads # ${VAR} substitution below (AGENTFEED_BIND_ADDR, AGENTFEED_HOST_DATA_DIR) reads
# from deploy/.env because Compose resolves .env relative to *this file's* # from deploy/.env because Compose resolves .env relative to *this file's*
# directory, not the caller's cwd. That relies on being invoked as # directory, not the caller's cwd. That relies on being invoked as
# `docker compose -f deploy/docker-compose.yml` from the repo root, per # `docker compose -f deploy/docker-compose.yml` from the repo root, per
@ -7,20 +7,20 @@
# the defaults (0.0.0.0 bind, ./data) would be a security regression, not # the defaults (0.0.0.0 bind, ./data) would be a security regression, not
# just a broken build. # just a broken build.
services: services:
daybook: agentfeed:
build: build:
context: .. context: ..
dockerfile: deploy/Dockerfile dockerfile: deploy/Dockerfile
container_name: daybook container_name: agentfeed
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env - .env
mem_limit: 384m mem_limit: 384m
mem_reservation: 128m mem_reservation: 128m
ports: ports:
- "${DAYBOOK_BIND_ADDR:-0.0.0.0}:8000:8000" - "${AGENTFEED_BIND_ADDR:-0.0.0.0}:8000:8000"
volumes: volumes:
- ${DAYBOOK_HOST_DATA_DIR:-./data}:/data - ${AGENTFEED_HOST_DATA_DIR:-./data}:/data
healthcheck: healthcheck:
# Hit the login page, not /api/capabilities: capabilities returns 403 # Hit the login page, not /api/capabilities: capabilities returns 403
# when unauthenticated, and `curl --fail` treats 403 as an error, which # when unauthenticated, and `curl --fail` treats 403 as an error, which

View file

@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -eu set -eu
mkdir -p "${DAYBOOK_DATA_DIR:-/data}" "${ARTIFACTS_ROOT:-/data/artifacts_store}" mkdir -p "${AGENTFEED_DATA_DIR:-/data}" "${ARTIFACTS_ROOT:-/data/artifacts_store}"
python manage.py migrate --noinput python manage.py migrate --noinput
python manage.py collectstatic --noinput python manage.py collectstatic --noinput

View file

@ -1,26 +1,26 @@
DJANGO_DEBUG=0 DJANGO_DEBUG=0
DJANGO_SECRET_KEY=replace-with-a-long-random-secret DJANGO_SECRET_KEY=replace-with-a-long-random-secret
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,your-vm-ip,your-domain.example DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,your-vm-ip,your-domain.example
# Required if Daybook sits behind a TLS-terminating reverse proxy (Apache, # Required if AgentFeed sits behind a TLS-terminating reverse proxy (Apache,
# Nginx, Traefik, a cloud load balancer). Without it, login/POST fails with # Nginx, Traefik, a cloud load balancer). Without it, login/POST fails with
# "CSRF verification failed" because Django can't tell the original request # "CSRF verification failed" because Django can't tell the original request
# was HTTPS. Set to the public HTTPS origin(s) users actually visit: # was HTTPS. Set to the public HTTPS origin(s) users actually visit:
DJANGO_CSRF_TRUSTED_ORIGINS=https://your-domain.example DJANGO_CSRF_TRUSTED_ORIGINS=https://your-domain.example
# Leave OFF in deployment. When on, POST /api/keys can create/list/revoke API # Disabled by default. When on, POST /api/keys can create/list/revoke API keys
# keys over the network (it returns key material), which is only appropriate # over the network (it returns key material), which is only appropriate for
# for automated test installations. Unset, it follows DJANGO_DEBUG — so with # automated test installations. Leave unset in deployment; set to 1 only on a
# DJANGO_DEBUG=0 above it is already off. Set to 1 only on a throwaway test box. # throwaway test box.
# DAYBOOK_KEY_PROVISIONING_API=0 # AGENTFEED_KEY_PROVISIONING_API=0
DAYBOOK_DATA_DIR=/data AGENTFEED_DATA_DIR=/data
DATABASE_PATH=/data/db.sqlite3 DATABASE_PATH=/data/db.sqlite3
ARTIFACTS_ROOT=/data/artifacts_store ARTIFACTS_ROOT=/data/artifacts_store
# Optional, read by deploy/docker-compose.yml directly (not by the app): # Optional, read by deploy/docker-compose.yml directly (not by the app):
# DAYBOOK_BIND_ADDR=127.0.0.1 # AGENTFEED_BIND_ADDR=127.0.0.1
# Uncomment and set to 127.0.0.1 when a reverse proxy on the same host will # Uncomment and set to 127.0.0.1 when a reverse proxy on the same host will
# front this service — this stops the container port from being reachable # front this service — this stops the container port from being reachable
# directly from the network. Leave unset (binds all interfaces) only for # directly from the network. Leave unset (binds all interfaces) only for
# private/VPN-only deployments with no proxy in front. # private/VPN-only deployments with no proxy in front.
# DAYBOOK_HOST_DATA_DIR=/mnt/some-larger-volume/daybook-data # AGENTFEED_HOST_DATA_DIR=/mnt/some-larger-volume/agentfeed-data
# Uncomment and point at a path on a separate/larger volume if the host's # Uncomment and point at a path on a separate/larger volume if the host's
# main disk is space-constrained. Defaults to ./data next to this file. # main disk is space-constrained. Defaults to ./data next to this file.

View file

@ -1,6 +1,6 @@
# Agent Quickstart # Agent Quickstart
This is the shortest path for an agent to publish and retrieve artifacts from Daybook. This is the shortest path for an agent to publish and retrieve artifacts from AgentFeed.
## 0. Get Your API Key ## 0. Get Your API Key

View file

@ -1,6 +1,6 @@
# Daybook API # AgentFeed API
Daybook is built around agents publishing artifacts. Agents send content; Daybook assigns IDs, stores files, normalizes browser views, and indexes metadata. AgentFeed is built around agents publishing artifacts. Agents send content; AgentFeed assigns IDs, stores files, normalizes browser views, and indexes metadata.
Base URL while running locally: Base URL while running locally:
@ -28,7 +28,7 @@ Currently supported kinds:
| Kind | Upload type | Rendered behavior | Raw support | | Kind | Upload type | Rendered behavior | Raw support |
|---|---|---|---| |---|---|---|---|
| `html_fragment` | JSON text | Wrapped with a Daybook theme; tables styled automatically; **HTML sanitized** | No | | `html_fragment` | JSON text | Wrapped with a AgentFeed theme; tables styled automatically; **HTML sanitized** | No |
| `html_full` | JSON text | Stored as complete HTML, verbatim — no theme, styling, or sanitization | Yes | | `html_full` | JSON text | Stored as complete HTML, verbatim — no theme, styling, or sanitization | Yes |
| `markdown` | JSON text | Rendered to HTML (code blocks syntax-highlighted, tables styled), **sanitized**, original `.md` preserved | Yes | | `markdown` | JSON text | Rendered to HTML (code blocks syntax-highlighted, tables styled), **sanitized**, original `.md` preserved | Yes |
| `deck` | JSON text | Wrapped as a simple HTML slide/deck page; **HTML sanitized** | No | | `deck` | JSON text | Wrapped as a simple HTML slide/deck page; **HTML sanitized** | No |
@ -482,7 +482,7 @@ Raw currently exists for:
| `json` | Preserved original JSON text. | | `json` | Preserved original JSON text. |
| `csv` | Preserved original CSV text. | | `csv` | Preserved original CSV text. |
If raw content is unavailable, Daybook returns a small HTML fallback page. If raw content is unavailable, AgentFeed returns a small HTML fallback page.
## Sharing ## Sharing
@ -503,7 +503,7 @@ GET /s/{share_token}
with the link can view it. with the link can view it.
- Images embedded in a shared artifact's content are also served - Images embedded in a shared artifact's content are also served
unauthenticated while that artifact is shared, so inline images don't unauthenticated while that artifact is shared, so inline images don't
break for a visitor without a Daybook account. break for a visitor without a AgentFeed account.
- `is_shared` and `share_url` (both in every artifact response — see - `is_shared` and `share_url` (both in every artifact response — see
`GET /api/artifacts/{id}`) report current sharing status; `share_url` is `GET /api/artifacts/{id}`) report current sharing status; `share_url` is
`null` when not shared. `null` when not shared.
@ -540,7 +540,7 @@ Example response:
This is generated from the server's actual configuration, so it's the This is generated from the server's actual configuration, so it's the
authoritative source if this document and a running instance ever disagree. authoritative source if this document and a running instance ever disagree.
Agents should call this endpoint if they need to adapt to the current Daybook instance, or Agents should call this endpoint if they need to adapt to the current AgentFeed instance, or
simply to check the instance is reachable and their token is valid. Unlike the other read simply to check the instance is reachable and their token is valid. Unlike the other read
endpoints, it accepts a bearer token as well as a browser session: endpoints, it accepts a bearer token as well as a browser session:
@ -557,11 +557,11 @@ GET /api/skill
GET /api/skill?format=json GET /api/skill?format=json
``` ```
Returns the bundled agent-posting skill (`skills/daybook-posting/SKILL.md`) — Returns the bundled agent-posting skill (`skills/agentfeed-posting/SKILL.md`) —
the same instructions you'd give an AI so it knows how to publish to this 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 instance. The server rewrites the `AGENTFEED_URL` line to **this instance's own
origin**, so the text a caller receives already points at the right server; origin**, so the text a caller receives already points at the right server;
a recipient only has to fill in their `DAYBOOK_TOKEN`. a recipient only has to fill in their `AGENTFEED_TOKEN`.
Same auth as capabilities (a browser session **or** a bearer token), so a 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 logged-in user can copy it to hand to their agent, or an agent can fetch it
@ -582,11 +582,11 @@ For automated test installations, a programmatic surface can provision them
too — but it is **gated** and **off in deployment**, because it returns key too — but it is **gated** and **off in deployment**, because it returns key
material over the API. material over the API.
The gate is `DAYBOOK_KEY_PROVISIONING_API`. Unset, it follows `DJANGO_DEBUG` The gate is `AGENTFEED_KEY_PROVISIONING_API`. It is **disabled by default** and
(on locally, off in production); set it explicitly (`0`/`1`) to override. independent of `DJANGO_DEBUG`; set `AGENTFEED_KEY_PROVISIONING_API=1` to enable it
`GET /api/capabilities` reports the current state as `key_provisioning_api`, (test installations only). `GET /api/capabilities` reports the current state as
and only lists the `/api/keys` methods when it's enabled. When disabled, every `key_provisioning_api`, and only lists the `/api/keys` methods when it's
method below returns `403`. enabled. When disabled, every method below returns `403`.
```http ```http
GET /api/keys # list this user's keys (incl. key values) GET /api/keys # list this user's keys (incl. key values)

View file

@ -1,6 +1,6 @@
# Deployment # Deployment
Daybook deploys the same way on any Linux host with Docker: a cloud VM, a home AgentFeed deploys the same way on any Linux host with Docker: a cloud VM, a home
server, a Proxmox guest, whatever. It runs as a single Docker container server, a Proxmox guest, whatever. It runs as a single Docker container
(Gunicorn) with all persistent state — the SQLite database and artifact files — (Gunicorn) with all persistent state — the SQLite database and artifact files —
on a bind-mounted `./data` directory outside the image. on a bind-mounted `./data` directory outside the image.
@ -8,7 +8,7 @@ on a bind-mounted `./data` directory outside the image.
```text ```text
Target host (cloud VM, home server, ...) Target host (cloud VM, home server, ...)
Docker Engine Docker Engine
daybook container agentfeed container
/app application code baked into the image /app application code baked into the image
/data persistent volume: db.sqlite3, artifacts_store/ /data persistent volume: db.sqlite3, artifacts_store/
``` ```
@ -17,8 +17,8 @@ Target host (cloud VM, home server, ...)
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `Dockerfile` | Builds the Daybook image. | | `Dockerfile` | Builds the AgentFeed image. |
| `docker-compose.yml` | Runs Daybook with a persistent `./data:/data` bind mount. | | `docker-compose.yml` | Runs AgentFeed with a persistent `./data:/data` bind mount. |
| `entrypoint.sh` | Runs migrations, collects static files, then starts Gunicorn. | | `entrypoint.sh` | Runs migrations, collects static files, then starts Gunicorn. |
| `env.example` | Template for deployment environment variables. | | `env.example` | Template for deployment environment variables. |
@ -59,12 +59,12 @@ Edit `deploy/.env` and set at minimum:
DJANGO_DEBUG=0 DJANGO_DEBUG=0
DJANGO_SECRET_KEY=<generated-secret> DJANGO_SECRET_KEY=<generated-secret>
DJANGO_ALLOWED_HOSTS=<vm-ip-or-dns-name>,localhost,127.0.0.1 DJANGO_ALLOWED_HOSTS=<vm-ip-or-dns-name>,localhost,127.0.0.1
DAYBOOK_DATA_DIR=/data AGENTFEED_DATA_DIR=/data
DATABASE_PATH=/data/db.sqlite3 DATABASE_PATH=/data/db.sqlite3
ARTIFACTS_ROOT=/data/artifacts_store ARTIFACTS_ROOT=/data/artifacts_store
``` ```
There is no shared API token to configure — Daybook uses per-user API keys There is no shared API token to configure — AgentFeed uses per-user API keys
created under Settings → API Keys once a user exists (each user can hold created under Settings → API Keys once a user exists (each user can hold
several, one per agent, each individually revocable). Do not commit several, one per agent, each individually revocable). Do not commit
`deploy/.env`. `deploy/.env`.
@ -80,7 +80,7 @@ Create your first login (superuser) so you have something to log in with and
a personal API token to post through: a personal API token to post through:
```bash ```bash
docker compose -f deploy/docker-compose.yml exec daybook python manage.py createsuperuser docker compose -f deploy/docker-compose.yml exec agentfeed python manage.py createsuperuser
``` ```
## Verify Deployment ## Verify Deployment
@ -106,7 +106,7 @@ curl -fsS -X POST http://127.0.0.1:8000/api/artifacts \
"source": "deploy-smoke", "source": "deploy-smoke",
"kind": "markdown", "kind": "markdown",
"title": "Deployment Smoke Test", "title": "Deployment Smoke Test",
"content": "# Deployment Smoke Test\n\nDaybook accepted an artifact." "content": "# Deployment Smoke Test\n\nAgentFeed accepted an artifact."
}' }'
``` ```
@ -127,17 +127,17 @@ picked up automatically. `deploy/.env` and `deploy/data/` are untouched by
## Operational Commands ## Operational Commands
```bash ```bash
docker compose -f deploy/docker-compose.yml logs -f daybook docker compose -f deploy/docker-compose.yml logs -f agentfeed
docker compose -f deploy/docker-compose.yml restart daybook docker compose -f deploy/docker-compose.yml restart agentfeed
docker compose -f deploy/docker-compose.yml down docker compose -f deploy/docker-compose.yml down
docker compose -f deploy/docker-compose.yml up -d docker compose -f deploy/docker-compose.yml up -d
docker compose -f deploy/docker-compose.yml exec daybook python manage.py check docker compose -f deploy/docker-compose.yml exec agentfeed python manage.py check
docker compose -f deploy/docker-compose.yml exec daybook sh docker compose -f deploy/docker-compose.yml exec agentfeed sh
``` ```
## Django Admin (`/admin/`) ## Django Admin (`/admin/`)
Daybook ships Django's built-in admin at `/admin/` as an operator AgentFeed ships Django's built-in admin at `/admin/` as an operator
break-glass tool — for raw-data fixes: reset a password, inspect or delete break-glass tool — for raw-data fixes: reset a password, inspect or delete
any user's artifacts, adjust a quota, audit or revoke API keys. It is any user's artifacts, adjust a quota, audit or revoke API keys. It is
separate from the in-app **Settings** area, which is the everyday product separate from the in-app **Settings** area, which is the everyday product
@ -147,17 +147,17 @@ surface (see the in-app guide / `docs/` for normal use).
`is_staff = True`. The account you create with `createsuperuser` has it (plus `is_staff = True`. The account you create with `createsuperuser` has it (plus
full superuser rights). Note the two different admin concepts: full superuser rights). Note the two different admin concepts:
- `is_superuser` — Daybook's *in-app* admin (can reach Settings → Users). - `is_superuser` — AgentFeed's *in-app* admin (can reach Settings → Users).
- `is_staff` — can log into *Django's* `/admin/` site. - `is_staff` — can log into *Django's* `/admin/` site.
The `createsuperuser` account has **both**. A user promoted in-app via The `createsuperuser` account has **both**. A user promoted in-app via
Settings → Users ("Grant admin access") gets `is_superuser` only — so they Settings → Users ("Grant admin access") gets `is_superuser` only — so they
can administer Daybook through its own UI but **cannot** reach `/admin/`. can administer AgentFeed through its own UI but **cannot** reach `/admin/`.
Regular users have neither. To grant someone `/admin/` access, set staff Regular users have neither. To grant someone `/admin/` access, set staff
explicitly: explicitly:
```bash ```bash
docker compose -f deploy/docker-compose.yml exec daybook \ docker compose -f deploy/docker-compose.yml exec agentfeed \
python manage.py shell -c "from django.contrib.auth.models import User; u=User.objects.get(username='NAME'); u.is_staff=True; u.save()" python manage.py shell -c "from django.contrib.auth.models import User; u=User.objects.get(username='NAME'); u.is_staff=True; u.save()"
``` ```
@ -172,23 +172,23 @@ Back up `deploy/data/` — it contains the SQLite database and all artifact
files: files:
```bash ```bash
tar -czf daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data tar -czf agentfeed-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
``` ```
For a consistent snapshot during heavy writes, stop the container first: For a consistent snapshot during heavy writes, stop the container first:
```bash ```bash
docker compose -f deploy/docker-compose.yml down docker compose -f deploy/docker-compose.yml down
tar -czf daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data tar -czf agentfeed-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
docker compose -f deploy/docker-compose.yml up -d docker compose -f deploy/docker-compose.yml up -d
``` ```
To restore: stop Daybook, replace `deploy/data/` with the backup contents, To restore: stop AgentFeed, replace `deploy/data/` with the backup contents,
then start it again. then start it again.
## Reverse Proxy Notes ## Reverse Proxy Notes
If putting Daybook behind Nginx, Caddy, Traefik, or a cloud load balancer: If putting AgentFeed behind Nginx, Caddy, Traefik, or a cloud load balancer:
- Proxy to `http://<vm-ip>:8000`. - Proxy to `http://<vm-ip>:8000`.
- Add the public hostname to `DJANGO_ALLOWED_HOSTS`. - Add the public hostname to `DJANGO_ALLOWED_HOSTS`.
@ -200,8 +200,8 @@ If putting Daybook behind Nginx, Caddy, Traefik, or a cloud load balancer:
terminates TLS and forwards plain HTTP to the container. terminates TLS and forwards plain HTTP to the container.
```text ```text
DJANGO_ALLOWED_HOSTS=daybook.example.com,localhost,127.0.0.1 DJANGO_ALLOWED_HOSTS=agentfeed.example.com,localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=https://daybook.example.com DJANGO_CSRF_TRUSTED_ORIGINS=https://agentfeed.example.com
``` ```
Forwarding the proto header, by proxy: Forwarding the proto header, by proxy:
@ -220,7 +220,7 @@ Caddy: header_up X-Forwarded-Proto https
- If behind a reverse proxy: `DJANGO_CSRF_TRUSTED_ORIGINS` is set and the - If behind a reverse proxy: `DJANGO_CSRF_TRUSTED_ORIGINS` is set and the
proxy forwards `X-Forwarded-Proto: https` (login will 403 otherwise). proxy forwards `X-Forwarded-Proto: https` (login will 403 otherwise).
- `deploy/data/` exists and persists across `git pull` + rebuild. - `deploy/data/` exists and persists across `git pull` + rebuild.
- `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy. - `docker compose -f deploy/docker-compose.yml ps` shows `agentfeed` healthy.
- `GET /api/capabilities` returns 403 without auth, 200 with a valid token or - `GET /api/capabilities` returns 403 without auth, 200 with a valid token or
session (check the status code — don't use `curl -f`, which errors on 403). session (check the status code — don't use `curl -f`, which errors on 403).
- At least one superuser exists (`manage.py createsuperuser`) with a key - At least one superuser exists (`manage.py createsuperuser`) with a key

View file

@ -1,8 +1,8 @@
# LLM Posting Helper # LLM Posting Helper
Agents should publish to Daybook with `skills/daybook-posting/scripts/daybook_post.py` instead of hand-building HTTP, JSON, curl, or PowerShell multipart requests. Agents should publish to AgentFeed with `skills/agentfeed-posting/scripts/agentfeed_post.py` instead of hand-building HTTP, JSON, curl, or PowerShell multipart requests.
The project also keeps `scripts/daybook_post.py` as a compatibility wrapper. The project also keeps `scripts/agentfeed_post.py` as a compatibility wrapper.
The helper is intentionally zero-dependency and uses only Python's standard library. It works on Windows and Linux. The helper is intentionally zero-dependency and uses only Python's standard library. It works on Windows and Linux.
@ -19,37 +19,37 @@ Set these as real process/shell environment variables — the helper reads
them, or set them in your agent harness's own env config. them, or set them in your agent harness's own env config.
```text ```text
DAYBOOK_URL=http://127.0.0.1:8001 AGENTFEED_URL=http://127.0.0.1:8001
DAYBOOK_TOKEN=your-personal-api-token AGENTFEED_TOKEN=your-personal-api-token
DAYBOOK_SOURCE=agent AGENTFEED_SOURCE=agent
``` ```
`DAYBOOK_TOKEN` is a personal API key, created under Settings → API Keys in the app (one per agent, individually revocable) — not a key shared across users. Override these per environment. `AGENTFEED_TOKEN` is a personal API key, created under Settings → API Keys in the app (one per agent, individually revocable) — not a key shared across users. Override these per environment.
## Commands ## Commands
Check the instance: Check the instance:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --check python skills/agentfeed-posting/scripts/agentfeed_post.py --check
``` ```
Post Markdown from a file: Post Markdown from a file:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report
``` ```
Post Markdown from stdin: Post Markdown from stdin:
```bash ```bash
cat report.md | python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" cat report.md | python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report"
``` ```
Post a deck: Post a deck:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --kind deck --title "Deployment Plan" --content-file deck.html python skills/agentfeed-posting/scripts/agentfeed_post.py --kind deck --title "Deployment Plan" --content-file deck.html
``` ```
`deck.html` must contain top-level `<section>...</section>` blocks. `deck.html` must contain top-level `<section>...</section>` blocks.
@ -57,44 +57,44 @@ python skills/daybook-posting/scripts/daybook_post.py --kind deck --title "Deplo
Post an image/PDF/binary file: Post an image/PDF/binary file:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --file diagram.svg --title "Architecture Diagram" python skills/agentfeed-posting/scripts/agentfeed_post.py --file diagram.svg --title "Architecture Diagram"
python skills/daybook-posting/scripts/daybook_post.py --file report.pdf --title "Generated Report" python skills/agentfeed-posting/scripts/agentfeed_post.py --file report.pdf --title "Generated Report"
``` ```
Post CSV or JSON as an interactive table/tree (auto-mode detects the kind Post CSV or JSON as an interactive table/tree (auto-mode detects the kind
from the extension; explicit `--kind` works too): from the extension; explicit `--kind` works too):
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py results.csv python skills/agentfeed-posting/scripts/agentfeed_post.py results.csv
python skills/daybook-posting/scripts/daybook_post.py --kind json --title "Run Summary" --content-file summary.json python skills/agentfeed-posting/scripts/agentfeed_post.py --kind json --title "Run Summary" --content-file summary.json
``` ```
Post an `.xlsx` spreadsheet (converted server-side to a `csv` artifact — Post an `.xlsx` spreadsheet (converted server-side to a `csv` artifact —
active sheet only, saved formula results only, no formatting preserved): active sheet only, saved formula results only, no formatting preserved):
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py balance_sheet.xlsx python skills/agentfeed-posting/scripts/agentfeed_post.py balance_sheet.xlsx
``` ```
Render other code as a Markdown fenced block instead (no dedicated `code` Render other code as a Markdown fenced block instead (no dedicated `code`
kind yet): kind yet):
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --file script.py --as-markdown --title "Generated Script" python skills/agentfeed-posting/scripts/agentfeed_post.py --file script.py --as-markdown --title "Generated Script"
``` ```
Embed an image inside a markdown/HTML artifact (upload first, then Embed an image inside a markdown/HTML artifact (upload first, then
reference the printed URL in your content): reference the printed URL in your content):
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --upload-image chart.png python skills/agentfeed-posting/scripts/agentfeed_post.py --upload-image chart.png
# -> prints .../assets/<id>; put that in your markdown, then post normally # -> prints .../assets/<id>; put that in your markdown, then post normally
``` ```
Dry run: Dry run:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Draft" --content-file report.md --dry-run python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Draft" --content-file report.md --dry-run
``` ```
The artifact URL is printed as the final stdout line. The artifact URL is printed as the final stdout line.
@ -118,7 +118,7 @@ The helper validates common LLM mistakes before sending:
Use repeatable `--meta key=value` flags: Use repeatable `--meta key=value` flags:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py \ python skills/agentfeed-posting/scripts/agentfeed_post.py \
--kind markdown \ --kind markdown \
--title "Run Summary" \ --title "Run Summary" \
--content-file summary.md \ --content-file summary.md \
@ -129,7 +129,7 @@ python skills/daybook-posting/scripts/daybook_post.py \
Or pass a JSON object: Or pass a JSON object:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Run Summary" --content-file summary.md --meta-json '{"run_id":"run-001"}' python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Run Summary" --content-file summary.md --meta-json '{"run_id":"run-001"}'
``` ```
`--meta` and `--meta-json` are merged if both are given. Metadata lives in `--meta` and `--meta-json` are merged if both are given. Metadata lives in
@ -140,7 +140,7 @@ filtering).
| Flag | Default | Notes | | Flag | Default | Notes |
|---|---|---| |---|---|---|
| `--source` | `DAYBOOK_SOURCE` env, else `agent` | What produced this — an agent-profile, script, or person name. | | `--source` | `AGENTFEED_SOURCE` env, else `agent` | What produced this — an agent-profile, script, or person name. |
| `--source-type` | `agent` | One of `agent`, `person`, `system`. | | `--source-type` | `agent` | One of `agent`, `person`, `system`. |
| `--source-name` | The effective `--source` | Human-readable name, e.g. "Hermes Agent". | | `--source-name` | The effective `--source` | Human-readable name, e.g. "Hermes Agent". |
| `--source-id` | Empty | A stable external ID, if one exists. | | `--source-id` | Empty | A stable external ID, if one exists. |
@ -152,7 +152,7 @@ filtering).
A thin LLM-facing wrapper lives at: A thin LLM-facing wrapper lives at:
```text ```text
skills/daybook-posting/SKILL.md skills/agentfeed-posting/SKILL.md
``` ```
Its key instruction is: do not hand-build requests; use `scripts/daybook_post.py`. Its key instruction is: do not hand-build requests; use `scripts/agentfeed_post.py`.

View file

@ -1,28 +1,28 @@
# Daybook Posting Helper # AgentFeed Posting Helper
`scripts/daybook_post.py` is a compatibility wrapper for the canonical zero-dependency helper at `skills/daybook-posting/scripts/daybook_post.py`. `scripts/agentfeed_post.py` is a compatibility wrapper for the canonical zero-dependency helper at `skills/agentfeed-posting/scripts/agentfeed_post.py`.
The helper is for agents and scripts that need to publish artifacts to Daybook without hand-crafting JSON, curl, or multipart requests. The helper is for agents and scripts that need to publish artifacts to AgentFeed without hand-crafting JSON, curl, or multipart requests.
Configuration: Configuration:
```text ```text
DAYBOOK_URL=http://127.0.0.1:8001 AGENTFEED_URL=http://127.0.0.1:8001
DAYBOOK_TOKEN=your-personal-api-token AGENTFEED_TOKEN=your-personal-api-token
DAYBOOK_SOURCE=agent AGENTFEED_SOURCE=agent
``` ```
`DAYBOOK_TOKEN` is a personal per-user token from Settings → Account in the app, not shared across users. `AGENTFEED_TOKEN` is a personal per-user token from Settings → Account in the app, not shared across users.
Examples: Examples:
```bash ```bash
python skills/daybook-posting/scripts/daybook_post.py --check python skills/agentfeed-posting/scripts/agentfeed_post.py --check
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report
cat report.md | python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" cat report.md | python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report"
python skills/daybook-posting/scripts/daybook_post.py --kind deck --title "Plan" --content-file deck.html python skills/agentfeed-posting/scripts/agentfeed_post.py --kind deck --title "Plan" --content-file deck.html
python skills/daybook-posting/scripts/daybook_post.py --file diagram.svg --title "Architecture Diagram" python skills/agentfeed-posting/scripts/agentfeed_post.py --file diagram.svg --title "Architecture Diagram"
python skills/daybook-posting/scripts/daybook_post.py --file results.csv --as-markdown --title "Results CSV" python skills/agentfeed-posting/scripts/agentfeed_post.py --file results.csv --as-markdown --title "Results CSV"
``` ```
The artifact browser URL is printed as the final stdout line. The artifact browser URL is printed as the final stdout line.

16
scripts/agentfeed_post.py Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the AgentFeed posting helper.
The canonical helper lives inside the self-contained skill folder:
skills/agentfeed-posting/scripts/agentfeed_post.py
"""
import runpy
from pathlib import Path
HELPER = Path(__file__).resolve().parent.parent / "skills" / "agentfeed-posting" / "scripts" / "agentfeed_post.py"
if __name__ == "__main__":
runpy.run_path(str(HELPER), run_name="__main__")

View file

@ -1,16 +0,0 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the Daybook posting helper.
The canonical helper lives inside the self-contained skill folder:
skills/daybook-posting/scripts/daybook_post.py
"""
import runpy
from pathlib import Path
HELPER = Path(__file__).resolve().parent.parent / "skills" / "daybook-posting" / "scripts" / "daybook_post.py"
if __name__ == "__main__":
runpy.run_path(str(HELPER), run_name="__main__")

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Post one demo artifact of every kind, tagged, dated today. """Post one demo artifact of every kind, tagged, dated today.
Posts through the real ingest API using the existing daybook_post.py helper, Posts through the real ingest API using the existing agentfeed_post.py helper,
so seeded data goes through the same normalization as real agent output. so seeded data goes through the same normalization as real agent output.
Usage (from project root, with the dev server running): Usage (from project root, with the dev server running):
@ -13,7 +13,7 @@ import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
HELPER = Path(__file__).resolve().parent.parent / "skills" / "daybook-posting" / "scripts" / "daybook_post.py" HELPER = Path(__file__).resolve().parent.parent / "skills" / "agentfeed-posting" / "scripts" / "agentfeed_post.py"
PERSONA = "seed-demo" PERSONA = "seed-demo"
TAGS = "seed-demo,demo" TAGS = "seed-demo,demo"

View file

@ -1,13 +1,13 @@
--- ---
name: daybook-posting name: agentfeed-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 a AgentFeed instance and returns a URL.
--- ---
# Daybook Posting # AgentFeed 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. AgentFeed 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. You post to a **running AgentFeed server**, which may be anywhere — a cloud VM, a container, or a workstation. You do **not** need a checkout of AgentFeed's source. The only things that tie you to a particular instance are two environment variables: `AGENTFEED_URL` (where the server is) and `AGENTFEED_TOKEN` (who you are). Set those and the rest of this skill works unchanged regardless of where you or the server run.
## Configuration ## Configuration
@ -17,20 +17,20 @@ 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 AGENTFEED_URL=https://your-agentfeed-instance.example.com # your server's base URL
DAYBOOK_TOKEN=your-personal-api-key # from Settings -> API Keys AGENTFEED_TOKEN=your-personal-api-key # from Settings -> API Keys
DAYBOOK_SOURCE=agent # a stable label for what's posting AGENTFEED_SOURCE=agent # a stable label for what's posting
``` ```
- **`DAYBOOK_URL`** — the base URL of the Daybook instance you're posting to. - **`AGENTFEED_URL`** — the base URL of the AgentFeed instance you're posting to.
Set it to your server. If it's unset, the helper falls back 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 `http://127.0.0.1:8001`, which is only correct when AgentFeed is running
locally on the same machine (its development default). For any remote or locally on the same machine (its development default). For any remote or
deployed instance you **must** set this. deployed instance you **must** set this.
- **`DAYBOOK_TOKEN`** — a personal API key, created in the app under Settings → - **`AGENTFEED_TOKEN`** — a personal API key, created in the app under Settings →
API Keys (name one per agent so you can revoke them individually). Not a key API Keys (name one per agent so you can revoke them individually). Not a key
shared by everyone; artifacts posted with it land only in your feed. shared by everyone; artifacts posted with it land only in your feed.
- **`DAYBOOK_SOURCE`** — a free-text label for whatever is posting - **`AGENTFEED_SOURCE`** — a free-text label for whatever is posting
(agent-profile, script, or person name). Optional; can also be passed per (agent-profile, script, or person name). Optional; can also be passed per
call with `--source`. call with `--source`.
@ -41,29 +41,29 @@ PowerShell multipart, or inline shell content (a `curl` fallback for
environments without the helper is documented at the end — the script is environments without the helper is documented at the end — the script is
still preferred everywhere it's available). still preferred everywhere it's available).
The helper lives at **`scripts/daybook_post.py` inside this skill's own The helper lives at **`scripts/agentfeed_post.py` inside this skill's own
directory** — not in any project you happen to be working in. Invoke it by 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 its absolute path. For brevity, the examples below assume you've captured
that path once: that path once:
```bash ```bash
# Point this at the helper inside THIS skill directory (absolute path). # Point this at the helper inside THIS skill directory (absolute path).
DAYBOOK_POST=/abs/path/to/this/skill/scripts/daybook_post.py AGENTFEED_POST=/abs/path/to/this/skill/scripts/agentfeed_post.py
``` ```
Then: Then:
```bash ```bash
python "$DAYBOOK_POST" --check python "$AGENTFEED_POST" --check
python "$DAYBOOK_POST" --kind markdown --title "Build Report" --content-file report.md python "$AGENTFEED_POST" --kind markdown --title "Build Report" --content-file report.md
cat report.md | python "$DAYBOOK_POST" --kind markdown --title "Build Report" cat report.md | python "$AGENTFEED_POST" --kind markdown --title "Build Report"
python "$DAYBOOK_POST" --file diagram.svg --title "Architecture Diagram" python "$AGENTFEED_POST" --file diagram.svg --title "Architecture Diagram"
``` ```
The artifact URL is printed as the final stdout line. The artifact URL is printed as the final stdout line.
(If you *are* working inside a Daybook source checkout, a compatibility (If you *are* working inside a AgentFeed source checkout, a compatibility
wrapper also exists at `scripts/daybook_post.py` in the repo root, but the wrapper also exists at `scripts/agentfeed_post.py` in the repo root, but the
canonical helper is the one bundled in this skill.) canonical helper is the one bundled in this skill.)
## Kind Cheat Sheet ## Kind Cheat Sheet
@ -100,7 +100,7 @@ with no API equivalent at all yet.
## CDN Snippets For `html_full` ## CDN Snippets For `html_full`
`html_full` is Daybook's escape hatch for custom pages — nothing is `html_full` is AgentFeed's escape hatch for custom pages — nothing is
injected into it, so a poster wanting Tailwind or a charting library has to injected into it, so a poster wanting Tailwind or a charting library has to
supply its own `<link>`/`<script>` tags. Known-good ones: supply its own `<link>`/`<script>` tags. Known-good ones:
@ -113,13 +113,13 @@ supply its own `<link>`/`<script>` tags. Known-good ones:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
``` ```
**Offline caveat:** Daybook can run fully self-hosted/air-gapped. Any **Offline caveat:** AgentFeed can run fully self-hosted/air-gapped. Any
artifact that links a CDN script won't render fully (or at all, for artifact that links a CDN script won't render fully (or at all, for
Tailwind's in-browser compiler) without internet access. If the deployment Tailwind's in-browser compiler) without internet access. If the deployment
target is offline, don't reach for these — use `markdown`/`html_fragment` target is offline, don't reach for these — use `markdown`/`html_fragment`
instead, which are styled entirely server-side with no external requests. instead, which are styled entirely server-side with no external requests.
**Self-hosted alternative for tables/JSON — no CDN needed:** Daybook already **Self-hosted alternative for tables/JSON — no CDN needed:** AgentFeed already
vendors Grid.js and renderjson locally (used to render `csv`/`json` vendors Grid.js and renderjson locally (used to render `csv`/`json`
artifacts), and they're reachable as plain static files from any artifact, artifacts), and they're reachable as plain static files from any artifact,
including `html_full`: including `html_full`:
@ -138,7 +138,7 @@ tables/JSON; there's no vendored equivalent for Tailwind/Chart.js, so the
CDN snippets above are still the only option for those. CDN snippets above are still the only option for those.
Only use this for `html_full` specifically — never add CDN references to Only use this for `html_full` specifically — never add CDN references to
`markdown`/`html_fragment`/`deck`. Those kinds exist so Daybook, not the `markdown`/`html_fragment`/`deck`. Those kinds exist so AgentFeed, not the
poster, owns styling; injecting a default CDN habit into them would poster, owns styling; injecting a default CDN habit into them would
undermine that. undermine that.
@ -149,12 +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 "$DAYBOOK_POST" --upload-image chart.png python "$AGENTFEED_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.
# The printed URL is absolute against your DAYBOOK_URL, e.g. # The printed URL is absolute against your AGENTFEED_URL, e.g.
# ![chart]($DAYBOOK_URL/assets/ab12cd34ef56) # ![chart]($AGENTFEED_URL/assets/ab12cd34ef56)
python "$DAYBOOK_POST" report.md python "$AGENTFEED_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,15 +167,15 @@ 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 "$AGENTFEED_POST" results.csv
python "$DAYBOOK_POST" --kind json --title "Run Summary" --content-file summary.json python "$AGENTFEED_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 "$DAYBOOK_POST" balance_sheet.xlsx python "$AGENTFEED_POST" balance_sheet.xlsx
``` ```
The server converts it to a `csv` artifact automatically. Known The server converts it to a `csv` artifact automatically. Known
@ -196,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 "$DAYBOOK_POST" --file script.py --as-markdown --title "Generated Script" python "$AGENTFEED_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.
@ -208,7 +208,7 @@ All optional, all default to something reasonable if omitted:
| Flag | Default | Notes | | Flag | Default | Notes |
|---|---|---| |---|---|---|
| `--source-type` | `agent` | One of `agent`, `person`, `system`. | | `--source-type` | `agent` | One of `agent`, `person`, `system`. |
| `--source-name` | Same as the effective source (`--source`, or `DAYBOOK_SOURCE` if `--source` wasn't given) | Human-readable name, e.g. "Hermes Agent". | | `--source-name` | Same as the effective source (`--source`, or `AGENTFEED_SOURCE` if `--source` wasn't given) | Human-readable name, e.g. "Hermes Agent". |
| `--source-id` | Empty string | A stable external ID, if the poster has one. | | `--source-id` | Empty string | A stable external ID, if the poster has one. |
| `--source-prompt` | Empty string | The prompt/instruction that produced this artifact — useful for later tracing why something was posted. | | `--source-prompt` | Empty string | The prompt/instruction that produced this artifact — useful for later tracing why something was posted. |
@ -219,9 +219,9 @@ 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 AgentFeed is reachable.
- Make sure `DAYBOOK_URL` points at the right instance and `DAYBOOK_TOKEN` is set before posting anything real. - Make sure `AGENTFEED_URL` points at the right instance and `AGENTFEED_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 `AGENTFEED_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.
@ -236,8 +236,8 @@ with no Python). Text artifacts post as JSON to `POST /api/artifacts` with
bearer-token auth: bearer-token auth:
```bash ```bash
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \ curl -sS -X POST "$AGENTFEED_URL/api/artifacts" \
-H "Authorization: Bearer $DAYBOOK_TOKEN" \ -H "Authorization: Bearer $AGENTFEED_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"kind":"markdown","title":"Build Report","source":"agent","content":"# Hello\n\nBody text."}' -d '{"kind":"markdown","title":"Build Report","source":"agent","content":"# Hello\n\nBody text."}'
``` ```
@ -245,12 +245,12 @@ curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
File/image artifacts use multipart instead: File/image artifacts use multipart instead:
```bash ```bash
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \ curl -sS -X POST "$AGENTFEED_URL/api/artifacts" \
-H "Authorization: Bearer $DAYBOOK_TOKEN" \ -H "Authorization: Bearer $AGENTFEED_TOKEN" \
-F "kind=image" -F "title=Architecture Diagram" -F "source=agent" \ -F "kind=image" -F "title=Architecture Diagram" -F "source=agent" \
-F "file=@diagram.png" -F "file=@diagram.png"
``` ```
`GET $DAYBOOK_URL/api/capabilities` (no auth needed beyond a login/token per `GET $AGENTFEED_URL/api/capabilities` (no auth needed beyond a login/token per
that instance's config) lists supported kinds, methods, and the server that instance's config) lists supported kinds, methods, and the server
version if you need to confirm the contract before posting. version if you need to confirm the contract before posting.

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Post artifacts to Daybook without shell-specific JSON or multipart quoting. """Post artifacts to AgentFeed without shell-specific JSON or multipart quoting.
Zero third-party dependencies. Content comes from a file or stdin so agents do not Zero third-party dependencies. Content comes from a file or stdin so agents do not
need to inline multiline JSON/HTML/Markdown in shell commands. need to inline multiline JSON/HTML/Markdown in shell commands.
@ -26,11 +26,11 @@ DEFAULT_SOURCE = "agent"
def main(): def main():
args = parse_args() args = parse_args()
base_url = os.environ.get("DAYBOOK_URL", DEFAULT_URL).rstrip("/") base_url = os.environ.get("AGENTFEED_URL", DEFAULT_URL).rstrip("/")
# DAYBOOK_TOKEN is your personal per-user token (Settings -> Account in the app); # AGENTFEED_TOKEN is your personal per-user token (Settings -> API Keys in the app);
# there is no shared fallback token, so posting without it cannot work. # there is no shared fallback token, so posting without it cannot work.
token = os.environ.get("DAYBOOK_TOKEN", "") token = os.environ.get("AGENTFEED_TOKEN", "")
source = args.source or os.environ.get("DAYBOOK_SOURCE", DEFAULT_SOURCE) source = args.source or os.environ.get("AGENTFEED_SOURCE", DEFAULT_SOURCE)
if args.check: if args.check:
try: try:
@ -43,8 +43,8 @@ def main():
if not token: if not token:
print( print(
"error: DAYBOOK_TOKEN is not set. Get your personal API token from " "error: AGENTFEED_TOKEN is not set. Get your personal API token from "
f"{base_url}/settings/account/ and set DAYBOOK_TOKEN.", f"{base_url}/settings/api-keys/ and set AGENTFEED_TOKEN.",
file=sys.stderr, file=sys.stderr,
) )
return 2 return 2
@ -84,11 +84,11 @@ def main():
def parse_args(): def parse_args():
parser = argparse.ArgumentParser(description="Publish an artifact to Daybook.") parser = argparse.ArgumentParser(description="Publish an artifact to AgentFeed.")
parser.add_argument("path", nargs="?", help="Auto mode: one file path. Kind, content-type, and title are inferred. The simplest way to post.") parser.add_argument("path", nargs="?", help="Auto mode: one file path. Kind, content-type, and title are inferred. The simplest way to post.")
parser.add_argument("--kind", choices=sorted(TEXT_KINDS), help="Text artifact kind. Omit when using --file or a positional path.") parser.add_argument("--kind", choices=sorted(TEXT_KINDS), help="Text artifact kind. Omit when using --file or a positional path.")
parser.add_argument("--title", help="Artifact title. Required unless --check is used.") parser.add_argument("--title", help="Artifact title. Required unless --check is used.")
parser.add_argument("--source", help="Artifact source (agent-profile, script, or person name). Defaults to DAYBOOK_SOURCE or 'agent'.") parser.add_argument("--source", help="Artifact source (agent-profile, script, or person name). Defaults to AGENTFEED_SOURCE or 'agent'.")
parser.add_argument("--content-file", help="Read text artifact content from this file.") parser.add_argument("--content-file", help="Read text artifact content from this file.")
parser.add_argument("--file", help="Upload this file as image/pdf/binary, or render as markdown with --as-markdown.") parser.add_argument("--file", help="Upload this file as image/pdf/binary, or render as markdown with --as-markdown.")
parser.add_argument("--tags", help="Comma-separated tags.") parser.add_argument("--tags", help="Comma-separated tags.")
@ -352,7 +352,7 @@ def post_payload(base_url, token, payload, args):
def multipart_body(payload): def multipart_body(payload):
boundary = f"----daybook-{uuid.uuid4().hex}" boundary = f"----agentfeed-{uuid.uuid4().hex}"
chunks = [] chunks = []
upload_path = payload["upload_path"] upload_path = payload["upload_path"]
for key, value in payload.items(): for key, value in payload.items():
@ -394,11 +394,11 @@ def content_type_for(path):
def upload_asset(base_url, token, path): def upload_asset(base_url, token, path):
"""Upload an image and return its Daybook URL for embedding in content.""" """Upload an image and return its AgentFeed URL for embedding in content."""
if not path.is_file(): if not path.is_file():
raise ValueError(f"image file does not exist: {path}") raise ValueError(f"image file does not exist: {path}")
boundary = f"----daybook-{uuid.uuid4().hex}" boundary = f"----agentfeed-{uuid.uuid4().hex}"
chunks = [ chunks = [
f"--{boundary}\r\n".encode("utf-8"), f"--{boundary}\r\n".encode("utf-8"),
f'Content-Disposition: form-data; name="file"; filename="{path.name}"\r\n'.encode("utf-8"), f'Content-Disposition: form-data; name="file"; filename="{path.name}"\r\n'.encode("utf-8"),

View file

@ -1,4 +1,4 @@
{% load daybook_extras %} {% load agentfeed_extras %}
<header class="topbar"> <header class="topbar">
<div class="brandline"> <div class="brandline">
<a class="brand-link" href="{% url 'feed' %}"><img class="brand-logo" src="/static/logo.png" alt=""><h1>AgentFeed</h1></a> <a class="brand-link" href="{% url 'feed' %}"><img class="brand-logo" src="/static/logo.png" alt=""><h1>AgentFeed</h1></a>
@ -48,10 +48,10 @@
const isDark = document.documentElement.getAttribute("data-theme") === "dark"; const isDark = document.documentElement.getAttribute("data-theme") === "dark";
if (isDark) { if (isDark) {
document.documentElement.removeAttribute("data-theme"); document.documentElement.removeAttribute("data-theme");
try { localStorage.setItem("daybook.theme", "light"); } catch (e) {} try { localStorage.setItem("agentfeed.theme", "light"); } catch (e) {}
} else { } else {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
try { localStorage.setItem("daybook.theme", "dark"); } catch (e) {} try { localStorage.setItem("agentfeed.theme", "dark"); } catch (e) {}
} }
syncLabel(); syncLabel();
}); });

View file

@ -6,12 +6,12 @@
<title>AgentFeed</title> <title>AgentFeed</title>
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/daybook.css"> <link rel="stylesheet" href="/static/agentfeed.css">
<script> <script>
// Set the theme before first paint so dark-mode users never see a light flash. // Set the theme before first paint so dark-mode users never see a light flash.
(function () { (function () {
try { try {
if (localStorage.getItem("daybook.theme") === "dark") { if (localStorage.getItem("agentfeed.theme") === "dark") {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
} }
} catch (e) {} } catch (e) {}
@ -141,8 +141,8 @@
const sourceSelected = Boolean(feed.dataset.selectedSource); const sourceSelected = Boolean(feed.dataset.selectedSource);
const sizes = { const sizes = {
days: Number(localStorage.getItem("daybook.daysWidth")) || 144, days: Number(localStorage.getItem("agentfeed.daysWidth")) || 144,
artifacts: Number(localStorage.getItem("daybook.artifactsWidth")) || 280, artifacts: Number(localStorage.getItem("agentfeed.artifactsWidth")) || 280,
}; };
const applySizes = () => { const applySizes = () => {
@ -177,7 +177,7 @@
}; };
const up = () => { const up = () => {
localStorage.setItem(`daybook.${target}Width`, String(sizes[target])); localStorage.setItem(`agentfeed.${target}Width`, String(sizes[target]));
document.body.classList.remove("is-resizing"); document.body.classList.remove("is-resizing");
splitter.removeEventListener("pointermove", move); splitter.removeEventListener("pointermove", move);
splitter.removeEventListener("pointerup", up); splitter.removeEventListener("pointerup", up);

View file

@ -6,12 +6,12 @@
<title>Log in — AgentFeed</title> <title>Log in — AgentFeed</title>
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/daybook.css"> <link rel="stylesheet" href="/static/agentfeed.css">
<script> <script>
// Set the theme before first paint so dark-mode users never see a light flash. // Set the theme before first paint so dark-mode users never see a light flash.
(function () { (function () {
try { try {
if (localStorage.getItem("daybook.theme") === "dark") { if (localStorage.getItem("agentfeed.theme") === "dark") {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
} }
} catch (e) {} } catch (e) {}

View file

@ -4,7 +4,7 @@
<div class="about-head"> <div class="about-head">
<img class="about-logo" src="/static/logo.png" alt=""> <img class="about-logo" src="/static/logo.png" alt="">
<div> <div>
<h2>Daybook</h2> <h2>AgentFeed</h2>
<p class="about-version">Version {{ version }}</p> <p class="about-version">Version {{ version }}</p>
</div> </div>
</div> </div>

View file

@ -6,7 +6,7 @@
<h3>API Keys</h3> <h3>API Keys</h3>
<p> <p>
Posting to Daybook uses named API keys — one per agent or script, each Posting to AgentFeed uses named API keys — one per agent or script, each
revocable on its own. You have {{ key_count }} key{{ key_count|pluralize }}. revocable on its own. You have {{ key_count }} key{{ key_count|pluralize }}.
</p> </p>
<p><a href="{% url 'settings-api-keys' %}">Manage API keys &rarr;</a></p> <p><a href="{% url 'settings-api-keys' %}">Manage API keys &rarr;</a></p>

View file

@ -4,7 +4,7 @@
<h2>API Keys</h2> <h2>API Keys</h2>
<p> <p>
One key per agent or script. Name each key so you know which agent it's One key per agent or script. Name each key so you know which agent it's
for, copy it into that agent's <code>DAYBOOK_TOKEN</code>, and revoke keys for, copy it into that agent's <code>AGENTFEED_TOKEN</code>, and revoke keys
individually here. Use a key as individually here. Use a key as
<code>Authorization: Bearer &lt;key&gt;</code> when posting. <code>Authorization: Bearer &lt;key&gt;</code> when posting.
</p> </p>

View file

@ -6,12 +6,12 @@
<title>Settings — AgentFeed</title> <title>Settings — AgentFeed</title>
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/daybook.css"> <link rel="stylesheet" href="/static/agentfeed.css">
<script> <script>
// Set the theme before first paint so dark-mode users never see a light flash. // Set the theme before first paint so dark-mode users never see a light flash.
(function () { (function () {
try { try {
if (localStorage.getItem("daybook.theme") === "dark") { if (localStorage.getItem("agentfeed.theme") === "dark") {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
} }
} catch (e) {} } catch (e) {}

View file

@ -8,7 +8,7 @@ from artifacts.models import ApiKey
User = get_user_model() User = get_user_model()
@override_settings(DAYBOOK_KEY_PROVISIONING_API=True) @override_settings(AGENTFEED_KEY_PROVISIONING_API=True)
class ApiKeyProvisioningEnabledTestCase(APITestCase): class ApiKeyProvisioningEnabledTestCase(APITestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user(username="alice", password="pw") self.user = User.objects.create_user(username="alice", password="pw")
@ -78,7 +78,7 @@ class ApiKeyProvisioningEnabledTestCase(APITestCase):
self.assertEqual(response.data["methods"]["/api/keys"], ["GET", "POST"]) self.assertEqual(response.data["methods"]["/api/keys"], ["GET", "POST"])
@override_settings(DAYBOOK_KEY_PROVISIONING_API=False) @override_settings(AGENTFEED_KEY_PROVISIONING_API=False)
class ApiKeyProvisioningDisabledTestCase(APITestCase): class ApiKeyProvisioningDisabledTestCase(APITestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user(username="alice", password="pw") self.user = User.objects.create_user(username="alice", password="pw")

View file

@ -24,7 +24,7 @@ class ArtifactAPITestCase(APITestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="daybook-test-artifacts-")) cls.artifacts_root = Path(tempfile.mkdtemp(prefix="agentfeed-test-artifacts-"))
cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root) cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root)
cls.settings_override.enable() cls.settings_override.enable()
@ -96,9 +96,9 @@ class ArtifactAPITestCase(APITestCase):
body = response.content.decode() body = response.content.decode()
# Frontmatter is intact and the config placeholder is rewritten to this # Frontmatter is intact and the config placeholder is rewritten to this
# instance's origin (http://testserver under the test client). # instance's origin (http://testserver under the test client).
self.assertIn("name: daybook-posting", body) self.assertIn("name: agentfeed-posting", body)
self.assertIn("DAYBOOK_URL=http://testserver", body) self.assertIn("AGENTFEED_URL=http://testserver", body)
self.assertNotIn("your-daybook-instance.example.com", body) self.assertNotIn("your-agentfeed-instance.example.com", body)
def test_skill_json_format(self): def test_skill_json_format(self):
response = self.client.get( response = self.client.get(
@ -107,7 +107,7 @@ class ArtifactAPITestCase(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["version"], __version__) self.assertEqual(response.data["version"], __version__)
self.assertEqual(response.data["url"], "http://testserver") self.assertEqual(response.data["url"], "http://testserver")
self.assertIn("name: daybook-posting", response.data["skill"]) self.assertIn("name: agentfeed-posting", response.data["skill"])
def test_skill_with_session(self): def test_skill_with_session(self):
self.client.force_login(self.user) self.client.force_login(self.user)
@ -442,7 +442,7 @@ class ArtifactAPITestCase(APITestCase):
self.assertEqual(stored_html.count("</script>"), 3) self.assertEqual(stored_html.count("</script>"), 3)
# The escaped content must still round-trip to the exact original value. # The escaped content must still round-trip to the exact original value.
start = stored_html.index('id="daybook-data">') + len('id="daybook-data">') start = stored_html.index('id="agentfeed-data">') + len('id="agentfeed-data">')
end = stored_html.index("</script>", start) end = stored_html.index("</script>", start)
embedded = stored_html[start:end] embedded = stored_html[start:end]
self.assertEqual(json.loads(embedded)["value"], "</script><script>alert(1)</script>") self.assertEqual(json.loads(embedded)["value"], "</script><script>alert(1)</script>")

View file

@ -6,76 +6,76 @@ from pathlib import Path
SCRIPT_PATH = ( SCRIPT_PATH = (
Path(__file__).resolve().parents[2] Path(__file__).resolve().parents[2]
/ "skills" / "skills"
/ "daybook-posting" / "agentfeed-posting"
/ "scripts" / "scripts"
/ "daybook_post.py" / "agentfeed_post.py"
) )
def _load_module(): def _load_module():
spec = importlib.util.spec_from_file_location("daybook_post", SCRIPT_PATH) spec = importlib.util.spec_from_file_location("agentfeed_post", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
return module return module
daybook_post = _load_module() agentfeed_post = _load_module()
class SniffTextKindTestCase(unittest.TestCase): class SniffTextKindTestCase(unittest.TestCase):
def test_markdown_suffixes(self): def test_markdown_suffixes(self):
for suffix in (".md", ".markdown", ".txt"): for suffix in (".md", ".markdown", ".txt"):
self.assertEqual(daybook_post.sniff_text_kind(suffix, "anything"), "markdown") self.assertEqual(agentfeed_post.sniff_text_kind(suffix, "anything"), "markdown")
def test_json_suffix(self): def test_json_suffix(self):
self.assertEqual(daybook_post.sniff_text_kind(".json", '{"a": 1}'), "json") self.assertEqual(agentfeed_post.sniff_text_kind(".json", '{"a": 1}'), "json")
def test_csv_suffix(self): def test_csv_suffix(self):
self.assertEqual(daybook_post.sniff_text_kind(".csv", "a,b\n1,2"), "csv") self.assertEqual(agentfeed_post.sniff_text_kind(".csv", "a,b\n1,2"), "csv")
def test_html_full_sniffed_from_content(self): def test_html_full_sniffed_from_content(self):
self.assertEqual( self.assertEqual(
daybook_post.sniff_text_kind(".html", "<!doctype html><html></html>"), "html_full" agentfeed_post.sniff_text_kind(".html", "<!doctype html><html></html>"), "html_full"
) )
def test_deck_sniffed_from_content(self): def test_deck_sniffed_from_content(self):
self.assertEqual(daybook_post.sniff_text_kind(".html", "<section>slide</section>"), "deck") self.assertEqual(agentfeed_post.sniff_text_kind(".html", "<section>slide</section>"), "deck")
def test_html_fragment_is_the_fallback(self): def test_html_fragment_is_the_fallback(self):
self.assertEqual(daybook_post.sniff_text_kind(".html", "<p>hi</p>"), "html_fragment") self.assertEqual(agentfeed_post.sniff_text_kind(".html", "<p>hi</p>"), "html_fragment")
class ValidatePayloadTestCase(unittest.TestCase): class ValidatePayloadTestCase(unittest.TestCase):
def test_valid_json_passes(self): def test_valid_json_passes(self):
daybook_post.validate_payload({"kind": "json", "content": '{"a": 1}'}, None) # no raise agentfeed_post.validate_payload({"kind": "json", "content": '{"a": 1}'}, None) # no raise
def test_invalid_json_raises(self): def test_invalid_json_raises(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "json", "content": "{not json"}, None) agentfeed_post.validate_payload({"kind": "json", "content": "{not json"}, None)
def test_deck_without_section_raises(self): def test_deck_without_section_raises(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "deck", "content": "<h1>no slides</h1>"}, None) agentfeed_post.validate_payload({"kind": "deck", "content": "<h1>no slides</h1>"}, None)
def test_html_fragment_with_nested_html_raises(self): def test_html_fragment_with_nested_html_raises(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
daybook_post.validate_payload( agentfeed_post.validate_payload(
{"kind": "html_fragment", "content": "<html><body>x</body></html>"}, None {"kind": "html_fragment", "content": "<html><body>x</body></html>"}, None
) )
def test_html_full_without_html_tag_raises(self): def test_html_full_without_html_tag_raises(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "html_full", "content": "<p>x</p>"}, None) agentfeed_post.validate_payload({"kind": "html_full", "content": "<p>x</p>"}, None)
def test_blank_markdown_raises(self): def test_blank_markdown_raises(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "markdown", "content": " "}, None) agentfeed_post.validate_payload({"kind": "markdown", "content": " "}, None)
def test_csv_has_no_shape_check(self): def test_csv_has_no_shape_check(self):
daybook_post.validate_payload({"kind": "csv", "content": "anything at all"}, None) # no raise agentfeed_post.validate_payload({"kind": "csv", "content": "anything at all"}, None) # no raise
def test_upload_path_skips_all_checks(self): def test_upload_path_skips_all_checks(self):
daybook_post.validate_payload({"upload_path": Path("whatever.png")}, None) # no raise agentfeed_post.validate_payload({"upload_path": Path("whatever.png")}, None) # no raise
class ResolveAutoPathTestCase(unittest.TestCase): class ResolveAutoPathTestCase(unittest.TestCase):
@ -93,7 +93,7 @@ class ResolveAutoPathTestCase(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "data.json" path = Path(tmp) / "data.json"
path.write_text('{"a": 1}', encoding="utf-8") path.write_text('{"a": 1}', encoding="utf-8")
args = daybook_post.resolve_auto_path(self._args(path)) args = agentfeed_post.resolve_auto_path(self._args(path))
self.assertEqual(args.kind, "json") self.assertEqual(args.kind, "json")
self.assertEqual(args.content_file, str(path)) self.assertEqual(args.content_file, str(path))
@ -101,14 +101,14 @@ class ResolveAutoPathTestCase(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "data.csv" path = Path(tmp) / "data.csv"
path.write_text("a,b\n1,2", encoding="utf-8") path.write_text("a,b\n1,2", encoding="utf-8")
args = daybook_post.resolve_auto_path(self._args(path)) args = agentfeed_post.resolve_auto_path(self._args(path))
self.assertEqual(args.kind, "csv") self.assertEqual(args.kind, "csv")
def test_image_file_becomes_multipart_upload(self): def test_image_file_becomes_multipart_upload(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "chart.png" path = Path(tmp) / "chart.png"
path.write_bytes(b"\x89PNG\r\n") path.write_bytes(b"\x89PNG\r\n")
args = daybook_post.resolve_auto_path(self._args(path)) args = agentfeed_post.resolve_auto_path(self._args(path))
self.assertIsNone(args.kind) self.assertIsNone(args.kind)
self.assertEqual(args.file, str(path)) self.assertEqual(args.file, str(path))

View file

@ -16,7 +16,7 @@ class MakeRoomForTestCase(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="daybook-test-storage-")) cls.artifacts_root = Path(tempfile.mkdtemp(prefix="agentfeed-test-storage-"))
cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root) cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root)
cls.settings_override.enable() cls.settings_override.enable()

View file

@ -16,7 +16,7 @@ class SharingTestCase(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="daybook-test-sharing-")) cls.artifacts_root = Path(tempfile.mkdtemp(prefix="agentfeed-test-sharing-"))
cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root) cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root)
cls.settings_override.enable() cls.settings_override.enable()