ai docs
This commit is contained in:
@@ -0,0 +1,244 @@
|
|||||||
|
# API Guide for AI Agents
|
||||||
|
|
||||||
|
This app is a personal content-operations hub. It stores **Sites**,
|
||||||
|
**Social Accounts**, and **Posts**, and exposes them over a local JSON API
|
||||||
|
so an AI agent (Claude Code, Mistral, or any other LLM/tool-calling agent)
|
||||||
|
can read the user's data and drive the content lifecycle — draft, schedule,
|
||||||
|
publish, report back.
|
||||||
|
|
||||||
|
**There is no authentication on this API.** It is designed to run on the
|
||||||
|
local network only. Do not expose it to the public internet. Because there
|
||||||
|
is no auth, credentials (social account passwords, WordPress Application
|
||||||
|
Passwords) are returned in **plain text** by design — the agent is expected
|
||||||
|
to use them directly (e.g. to log into Reddit, or call WordPress's REST
|
||||||
|
API) without an extra decryption step.
|
||||||
|
|
||||||
|
Base URL: `{APP_URL}/api` — e.g. `http://localhost:8000/api` in local dev.
|
||||||
|
All requests/responses are JSON. Send `Accept: application/json` (Laravel
|
||||||
|
defaults to JSON error responses for API routes regardless, but set it to
|
||||||
|
be safe on validation-error bodies).
|
||||||
|
|
||||||
|
## Mental model
|
||||||
|
|
||||||
|
```
|
||||||
|
Site (a website the user owns)
|
||||||
|
└─ cms_type: wordpress | custom | other
|
||||||
|
└─ if wordpress: wp_username + wp_app_password (WP Application Password)
|
||||||
|
|
||||||
|
SocialAccount (login for a social network)
|
||||||
|
└─ network: twitter | instagram | facebook | reddit | tiktok | linkedin | youtube | other
|
||||||
|
|
||||||
|
Post (one piece of content, belongs to a Site, optionally a SocialAccount)
|
||||||
|
└─ type: same vocabulary as SocialAccount.network, plus "blog"
|
||||||
|
└─ status: draft -> scheduled -> published | failed
|
||||||
|
└─ published_url: set once it's actually live somewhere
|
||||||
|
```
|
||||||
|
|
||||||
|
The typical agent loop is:
|
||||||
|
|
||||||
|
1. Read Sites/Social Accounts to know what you're working with (`GET /api/sites`, `GET /api/accounts`).
|
||||||
|
2. Create a Post as a draft (`POST /api/posts`), or find one that's already `due` (`GET /api/posts?due=1`).
|
||||||
|
3. Publish it externally, by whatever mechanism fits its `type`/site (see "How to actually publish" below).
|
||||||
|
4. Report the outcome back with `PATCH /api/posts/{id}` — set `status` to `published` or `failed`, and `published_url` if you have one, **unless** you used the `posts:publish` command, which already does this step for you.
|
||||||
|
|
||||||
|
## Sites
|
||||||
|
|
||||||
|
Read-only. No create/update/delete via API — sites are managed through the
|
||||||
|
app's UI at `/sites`.
|
||||||
|
|
||||||
|
`GET /api/sites`
|
||||||
|
Optional query param: `?name=` (partial, case-sensitive-per-DB-collation match on name).
|
||||||
|
|
||||||
|
`GET /api/sites/{id}`
|
||||||
|
|
||||||
|
Response shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "My Blog",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"description": "A blog about things",
|
||||||
|
"cms_type": "wordpress",
|
||||||
|
"wp_username": "admin",
|
||||||
|
"wp_app_password": "abcd 1234 efgh 5678",
|
||||||
|
"created_at": "2026-07-19T12:00:00.000000Z",
|
||||||
|
"updated_at": "2026-07-19T12:00:00.000000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`cms_type` is one of `wordpress`, `custom`, `other`. `wp_username`/
|
||||||
|
`wp_app_password` are only meaningful (non-null) when `cms_type` is
|
||||||
|
`wordpress` — they're a WordPress **Application Password** pair (see
|
||||||
|
"WordPress publishing" below), not a regular account password.
|
||||||
|
|
||||||
|
## Social Accounts
|
||||||
|
|
||||||
|
Read-only, same reasoning as Sites — manage via `/social-accounts` in the UI.
|
||||||
|
|
||||||
|
`GET /api/accounts`
|
||||||
|
Optional query params: `?network=` (exact match), `?type=` (exact match).
|
||||||
|
|
||||||
|
`GET /api/accounts/{id}`
|
||||||
|
|
||||||
|
Response shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"network": "reddit",
|
||||||
|
"username": "someuser",
|
||||||
|
"email": "someuser@example.com",
|
||||||
|
"password": "plaintext-password",
|
||||||
|
"type": "post",
|
||||||
|
"created_at": "...",
|
||||||
|
"updated_at": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`network` is one of `twitter`, `instagram`, `facebook`, `reddit`, `tiktok`,
|
||||||
|
`linkedin`, `youtube`, `other`. `type` is one of `post`, `comment`, `like`,
|
||||||
|
or `null`. The `password` field is the real plain-text login password for
|
||||||
|
that network — treat it as a live credential.
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
Full CRUD — this is the resource the agent actively drives.
|
||||||
|
|
||||||
|
`GET /api/posts`
|
||||||
|
Query params (all optional, combine freely):
|
||||||
|
- `?status=` — exact match: `draft` | `scheduled` | `published` | `failed`
|
||||||
|
- `?site_id=` — exact match
|
||||||
|
- `?social_account_id=` — exact match
|
||||||
|
- `?due=1` — the important one for automation: returns posts where
|
||||||
|
`status=scheduled AND scheduled_at <= now()`. **Poll this to find work.**
|
||||||
|
|
||||||
|
`GET /api/posts/{id}`
|
||||||
|
|
||||||
|
`POST /api/posts` — create a post.
|
||||||
|
Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"site_id": 1,
|
||||||
|
"social_account_id": null,
|
||||||
|
"type": "blog",
|
||||||
|
"title": "Optional title",
|
||||||
|
"body": "Required content",
|
||||||
|
"status": "draft",
|
||||||
|
"scheduled_at": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Validation: `site_id` required, must exist. `social_account_id` nullable,
|
||||||
|
must exist if given. `type` required, one of `twitter`, `instagram`,
|
||||||
|
`facebook`, `reddit`, `tiktok`, `linkedin`, `youtube`, `blog`, `other`.
|
||||||
|
`body` required. `title` nullable. `status` defaults to `draft` if omitted.
|
||||||
|
`scheduled_at` nullable datetime.
|
||||||
|
|
||||||
|
Note: `type` and the linked Social Account's `network` are **not**
|
||||||
|
cross-validated — you can link a `type=blog` post to a Twitter account, or
|
||||||
|
no account at all. Set `type` to whatever actually describes the content;
|
||||||
|
it's what later determines *how* the post gets published (see below).
|
||||||
|
|
||||||
|
`PATCH /api/posts/{id}` — partial update. This is how you **report a
|
||||||
|
publish result** back for anything you didn't publish via the
|
||||||
|
`posts:publish` command:
|
||||||
|
```json
|
||||||
|
{ "status": "published", "published_url": "https://example.com/2026/07/my-post" }
|
||||||
|
```
|
||||||
|
or on failure:
|
||||||
|
```json
|
||||||
|
{ "status": "failed" }
|
||||||
|
```
|
||||||
|
|
||||||
|
`DELETE /api/posts/{id}`
|
||||||
|
|
||||||
|
## How to actually publish a post
|
||||||
|
|
||||||
|
This app is a data store and scheduler — **it does not publish content by
|
||||||
|
itself except via one specific command below.** Publishing mechanism
|
||||||
|
depends on the post's target:
|
||||||
|
|
||||||
|
### WordPress sites (`type=blog`, site has `cms_type=wordpress`)
|
||||||
|
|
||||||
|
Don't call the WordPress REST API yourself from a generic HTTP request —
|
||||||
|
use the built-in Artisan command, which handles auth, validation, and
|
||||||
|
status/`published_url` bookkeeping for you:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan posts:publish {post_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
1. Loads the Post and its Site.
|
||||||
|
2. Validates: `post.type` must be `blog`, `post.title` must be set,
|
||||||
|
`site.cms_type` must be `wordpress`, and the site must have
|
||||||
|
`wp_username`/`wp_app_password` set. Any failure here exits 1 with a
|
||||||
|
descriptive message and **does not touch** `Post.status`.
|
||||||
|
3. POSTs to `{site.url}/wp-json/wp/v2/posts` using HTTP Basic Auth with
|
||||||
|
the site's WordPress Application Password, `status: publish` (goes live
|
||||||
|
immediately, not a WP draft).
|
||||||
|
4. On success: sets `Post.status = published`, `Post.published_url` to the
|
||||||
|
new post's permalink, prints `{"success": true, "url": "..."}`, exits 0.
|
||||||
|
5. On failure (bad credentials, unreachable site, WordPress error): sets
|
||||||
|
`Post.status = failed`, prints `{"success": false, "error": "..."}`,
|
||||||
|
exits 1.
|
||||||
|
|
||||||
|
This is a **shell command**, not an HTTP endpoint — run it from wherever
|
||||||
|
you have shell access to this app (e.g. `php artisan posts:publish 42`).
|
||||||
|
You do not need to call `PATCH /api/posts/{id}` afterward; the command
|
||||||
|
already updates the Post's status and `published_url`.
|
||||||
|
|
||||||
|
If a WordPress site is missing `wp_username`/`wp_app_password`, the user
|
||||||
|
needs to generate an Application Password in their WP admin (**Users →
|
||||||
|
Profile → Application Passwords**) and set it on the Site via this app's
|
||||||
|
`/sites` UI — this is not something an agent can do on the user's behalf.
|
||||||
|
|
||||||
|
### Reddit (`type=reddit`)
|
||||||
|
|
||||||
|
Use the standalone sidecar at `sidecars/reddit-post/`, a Playwright script
|
||||||
|
(not part of this Laravel app, no shared DB access):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./sidecars/reddit-post/run.sh \
|
||||||
|
--username=USERNAME --password=PASSWORD \
|
||||||
|
--subreddit=test --title="..." --body="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
It logs into Reddit and creates a text post, printing the same
|
||||||
|
`{"success": ..., "url"/"error": ...}` JSON contract on stdout. **Unlike
|
||||||
|
`posts:publish`, this sidecar does not know about the Posts table at
|
||||||
|
all** — get the Social Account credentials from `GET /api/accounts`
|
||||||
|
first, then after the sidecar returns, report the result yourself via
|
||||||
|
`PATCH /api/posts/{id}` (`status` + `published_url` from the sidecar's
|
||||||
|
`url`, or `status: failed` if it errored).
|
||||||
|
|
||||||
|
### Any other network/CMS (`custom`, `other`, or a `type` with no
|
||||||
|
integration yet)
|
||||||
|
|
||||||
|
There is no built-in publishing path. Do it however is appropriate for
|
||||||
|
that platform (its own API, its own automation), using the credentials
|
||||||
|
from `GET /api/sites` / `GET /api/accounts`, then report back via
|
||||||
|
`PATCH /api/posts/{id}` exactly as described above. This is the general
|
||||||
|
fallback contract: **whatever publishes a post, always ends with either
|
||||||
|
the `posts:publish` command's automatic bookkeeping, or a manual `PATCH`
|
||||||
|
setting `status` and, if applicable, `published_url`.**
|
||||||
|
|
||||||
|
## Quick reference
|
||||||
|
|
||||||
|
| Resource | List | Get one | Create | Update | Delete |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Sites | `GET /api/sites` | `GET /api/sites/{id}` | — | — | — |
|
||||||
|
| Social Accounts | `GET /api/accounts` | `GET /api/accounts/{id}` | — | — | — |
|
||||||
|
| Posts | `GET /api/posts` | `GET /api/posts/{id}` | `POST /api/posts` | `PATCH/PUT /api/posts/{id}` | `DELETE /api/posts/{id}` |
|
||||||
|
|
||||||
|
| Task | How |
|
||||||
|
|---|---|
|
||||||
|
| Find work to publish | `GET /api/posts?due=1` |
|
||||||
|
| Publish to a WordPress site | `php artisan posts:publish {post_id}` (handles status/published_url itself) |
|
||||||
|
| Publish to Reddit | `sidecars/reddit-post/run.sh ...`, then `PATCH /api/posts/{id}` yourself |
|
||||||
|
| Publish anywhere else | do it yourself, then `PATCH /api/posts/{id}` yourself |
|
||||||
|
| Report a failed publish | `PATCH /api/posts/{id}` with `{"status": "failed"}` |
|
||||||
Reference in New Issue
Block a user