From 4bd21f3234b9e986bdc00eeabca29a8772a9d81b Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Sun, 19 Jul 2026 17:23:06 +0300 Subject: [PATCH] ai docs --- AI_API.md | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 AI_API.md diff --git a/AI_API.md b/AI_API.md new file mode 100644 index 0000000..fae9b7a --- /dev/null +++ b/AI_API.md @@ -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"}` |