@dastyaresocial/ds-cs 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +134 -2
  2. package/bin/ds-cs.mjs +14 -0
  3. package/package.json +33 -4
  4. package/skills/README.md +71 -0
  5. package/skills/dev/add-component/SKILL.md +166 -0
  6. package/skills/dev/add-orpc-endpoint/SKILL.md +143 -0
  7. package/skills/dev/db-migration/SKILL.md +104 -0
  8. package/skills/dev/release/SKILL.md +106 -0
  9. package/skills/product/brand-config/SKILL.md +98 -0
  10. package/skills/product/manage-stories/SKILL.md +119 -0
  11. package/skills/product/mcp-connect/SKILL.md +116 -0
  12. package/skills/product/post-with-media/SKILL.md +197 -0
  13. package/skills/product/self-host-deploy/SKILL.md +112 -0
  14. package/skills/product/webhooks/SKILL.md +160 -0
  15. package/src/agents.mjs +130 -0
  16. package/src/args.mjs +111 -0
  17. package/src/auth.mjs +82 -0
  18. package/src/commands/add.mjs +118 -0
  19. package/src/commands/auth.mjs +87 -0
  20. package/src/commands/init.mjs +106 -0
  21. package/src/commands/list.mjs +89 -0
  22. package/src/commands/mcp.mjs +101 -0
  23. package/src/commands/remove.mjs +80 -0
  24. package/src/commands/search.mjs +51 -0
  25. package/src/commands/target.mjs +83 -0
  26. package/src/commands/update.mjs +94 -0
  27. package/src/index.mjs +176 -0
  28. package/src/install.mjs +174 -0
  29. package/src/lockfile.mjs +74 -0
  30. package/src/mcp-config.mjs +39 -0
  31. package/src/registry/archive.mjs +209 -0
  32. package/src/registry/fetch.mjs +114 -0
  33. package/src/registry/sources.mjs +395 -0
  34. package/src/skills/discover.mjs +170 -0
  35. package/src/skills/frontmatter.mjs +85 -0
  36. package/src/utils/errors.mjs +13 -0
  37. package/src/utils/fs.mjs +106 -0
  38. package/src/utils/log.mjs +85 -0
  39. package/src/utils/prompt.mjs +69 -0
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: release
3
+ description: Cut a DS-CS release — version bump in package.json, selective staging, commit, annotated tag, and push. Use whenever someone says "ship it", "release", "stage and push changes", "bump the version", or asks to publish a new version. Also covers what makes the in-app update banner fire.
4
+ ---
5
+
6
+ # Cutting a release
7
+
8
+ ## The rule that overrides everything
9
+
10
+ **"Stage and push changes" is a release, not a commit.** A bare commit is never the right response. A version bump is always included; never push without one.
11
+
12
+ ## The sequence, in order
13
+
14
+ ### 1. Review first
15
+
16
+ ```bash
17
+ git status
18
+ git diff
19
+ git diff --cached
20
+ ```
21
+
22
+ Decide what is actually being released and whether anything unrelated is in the working tree. You must be able to state this before touching the index.
23
+
24
+ ### 2. Stage selectively
25
+
26
+ Stage only the files belonging to this change. If unrelated edits are present, leave them unstaged and **say so explicitly** — never sweep them in silently.
27
+
28
+ ```bash
29
+ git add <specific paths>
30
+ git status # confirm what is actually staged
31
+ ```
32
+
33
+ ### 3. Bump the version
34
+
35
+ Read the current version from `package.json` — it is the source of truth.
36
+
37
+ ```bash
38
+ npm version patch # or minor / major
39
+ ```
40
+
41
+ `next.config.ts` bakes it into the build as `NEXT_PUBLIC_APP_VERSION`, which is what drives the in-app update banner. Bumping the number without rebuilding means users never see the update prompt; that is expected, not a bug — the banner appears when a *later* build ships.
42
+
43
+ | Change | Bump | Example |
44
+ |---|---|---|
45
+ | Bug fix, refactor, internal/infra change with no user-visible effect | **patch** | `v0.3.3` → `v0.3.4` |
46
+ | New feature, new endpoint, new capability | **minor** | `v0.3.4` → `v0.4.0` |
47
+ | Breaking change — removed/renamed public API, config or env var changes, DB migrations that break existing data | **major** | `v0.4.0` → `v1.0.0` |
48
+
49
+ Judge the bump **from the diff**, not from the commit message.
50
+
51
+ - Never skip a number.
52
+ - Do not bump for a bump alone. If the diff is empty or contains only whitespace, stop and ask.
53
+ - If the change sits between two levels, choose the **lower** one and mention the alternative in your report.
54
+
55
+ ### 4. Commit and push
56
+
57
+ ```bash
58
+ git commit -m "<type>(<scope>): <summary>"
59
+ git push origin main
60
+ ```
61
+
62
+ ### 5. Tag the commit you just pushed
63
+
64
+ ```bash
65
+ git tag -l # existing tags
66
+ git tag -l v0.3.4 # must be empty before you create it
67
+ git tag -a v0.3.4 -m "v0.3.4"
68
+ git push origin main --follow-tags
69
+ ```
70
+
71
+ Tags are annotated (`-a`) and named `vMAJOR.MINOR.PATCH`. The tag points at the commit that carries that version.
72
+
73
+ **Never move or force-update a tag that has already been pushed.** If the tag already exists, stop and ask.
74
+
75
+ ### 6. Report
76
+
77
+ State the new version, the tag, and anything you deliberately left out of the release. If you staged selectively, name what you left behind.
78
+
79
+ ## Deploy surfaces
80
+
81
+ | Surface | Trigger |
82
+ |---|---|
83
+ | Docker (`dastyaresocial/ds-cs`) | Image published from `main` after the tag lands |
84
+ | Vercel | `next build` runs `generate:config`, `generate:icons`, `upload:emojis`, `db:migrate`, `bootstrap:admin` |
85
+
86
+ `bun run build` is not just a bundle — it runs migrations and bootstraps the admin user. A build that succeeds locally has already touched the database if `DATABASE_URL` was set. That is why migration files must be committed before a release, not after.
87
+
88
+ ## Also worth doing
89
+
90
+ ```bash
91
+ # Keep the changelog in step with the version
92
+ $EDITOR CHANGELOG.md
93
+ ```
94
+
95
+ Commit the changelog entry with the release, not as a follow-up commit — a tag that points at a commit missing its changelog entry is a release nobody can reconstruct.
96
+
97
+ ## Verify before you report done
98
+
99
+ ```bash
100
+ bun run test
101
+ bun run lint
102
+ git log --oneline -3
103
+ git tag -l "v$(npm pkg get version | tr -d '"')"
104
+ ```
105
+
106
+ The last command should print the tag you just created.
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: brand-config
3
+ description: Configure a DS-CS channel's identity — username, name, description, the /about resume page, and the profile image. Use when rebranding a channel, editing config/app.config.yml or config/about.config.yml, toggling the about page, or when someone asks to change the site name or bio. Changes apply on save with no rebuild or restart.
4
+ ---
5
+
6
+ # Branding a channel
7
+
8
+ ## The live-edit model
9
+
10
+ Three files control the channel's identity:
11
+
12
+ | File | Controls | Applied by |
13
+ |---|---|---|
14
+ | `config/app.config.yml` | Username, email, channel name, description, meta, header, profile | reload the page |
15
+ | `config/about.config.yml` | The `/about` page — enabled flag, bio, contacts, work/education sections | refresh `/about` |
16
+ | `public/profile-image.png` | The channel avatar | reload |
17
+
18
+ All three are read from disk on **every request**. Save the file, reload the browser, and the change is live. **No rebuild. No `docker compose restart`.** Hard-refresh to bypass caching.
19
+
20
+ `docker-compose.yml` bind-mounts the whole `./config` directory rather than individual files, so editors that save via atomic rename (VS Code, most others) cannot orphan the inode the container is watching.
21
+
22
+ ## app.config.yml
23
+
24
+ ```yaml
25
+ general:
26
+ username: "omidshabab"
27
+ email: "hey@example.com"
28
+
29
+ en:
30
+ name: "Your Name"
31
+ desc: "Short description used in meta tags and on the /about page."
32
+ ```
33
+
34
+ `general.username` is the account handle. `general.email` is the contact shown in the footer and contact links. Each locale key (`en`, and any others you add) carries `name` and `desc`; `name` drives the browser tab, the header, and SEO, and `desc` drives meta tags and `/about`.
35
+
36
+ This file is compiled to `config/app.config.json` at build time by `bun run generate:config`, and the JSON twin is what the client bundle imports. The runtime editor path re-reads the YAML on each request, which is what makes live edits work without a rebuild.
37
+
38
+ ## about.config.yml
39
+
40
+ The first key **must** be `enabled:`, and `general.name` must be non-empty — the API enforces both.
41
+
42
+ ```yaml
43
+ enabled: true
44
+ general:
45
+ name: "Your Name"
46
+ jobTitle: "Builder"
47
+ website: "https://example.com"
48
+ about: "A longer bio."
49
+ contacts:
50
+ - label: "Email"
51
+ value: "you@example.com"
52
+ content:
53
+ - title: "Work Experience"
54
+ items:
55
+ - title: "Role"
56
+ company: "Company"
57
+ period: "2024 — present"
58
+ description: "What you did."
59
+ - title: "Education"
60
+ items:
61
+ - title: "Degree"
62
+ school: "University"
63
+ period: "2020 — 2024"
64
+ ```
65
+
66
+ When `enabled` is false, `/about` renders the not-found page and leaves the sitemap. **All other data is preserved** — toggling visibility is a one-key flip, not a rewrite.
67
+
68
+ ### Via the API or MCP
69
+
70
+ ```bash
71
+ # read the current config
72
+ curl https://cs.example.com/api/... # or use the get_resume_config MCP tool
73
+
74
+ # toggle visibility without touching content — preferred
75
+ # MCP tool: set_resume_enabled { "enabled": false }
76
+ ```
77
+
78
+ `update_resume_config` replaces the **whole file** from a YAML string, so read the current config first and edit it — a blind overwrite silently discards every section. `set_resume_enabled` is the safe operation for visibility: it flips one key and preserves everything else.
79
+
80
+ ## Profile image
81
+
82
+ Replace `public/profile-image.png` in place. It is served at `/profile-image.png` with `no-store`, your file wins over the baked default, and if the file is missing at startup the default is seeded in so the URL always resolves. `docker compose up` never copies over it.
83
+
84
+ ## Verify
85
+
86
+ ```bash
87
+ docker compose logs -f app # config parse errors surface here
88
+ ```
89
+
90
+ Then reload the site and `/about`. If a change did not appear, the YAML is the thing to suspect first — an indentation error or a missing `general.name` makes the config fall back rather than crash, which reads as "my edit did nothing".
91
+
92
+ ## Mistakes that cost time
93
+
94
+ - Restarting Docker after a config edit. Unnecessary, and it hides the real problem.
95
+ - Putting `enabled:` anywhere but the first line of `about.config.yml` — the API rejects it.
96
+ - Calling `update_resume_config` with a partial document and wiping the resume.
97
+ - Editing `config/app.config.json` directly. It is generated; the YAML is the source of truth and your edit is overwritten on the next `generate:config`.
98
+ - Replacing `profile-image.png` with a different filename. The route serves that exact path.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: manage-stories
3
+ description: Create, list, view, like, update and delete ephemeral stories on a DS-CS channel over REST or MCP. Use for 24-hour content, vertical video or image stories, story counters, or cleaning up expired story ids. For feed posts use post-with-media; for realtime notifications use webhooks.
4
+ ---
5
+
6
+ # Working with stories
7
+
8
+ ## How stories differ from posts
9
+
10
+ Stories are **ephemeral** — short-lived, single-media, portrait-first. They have no `content` field, no pin, and no reactions; they have `views` and `likes` counters instead. Everything else — auth, media upload, the API-key rate limit — is identical to posts.
11
+
12
+ ```
13
+ Auth: Authorization: Bearer <API_KEY> (writes only; reads are public)
14
+ Media: upload first, then reference the returned URL by media type
15
+ ```
16
+
17
+ ## Upload the media first
18
+
19
+ Stories only accept `image` and `video`. Use the same upload endpoints as posts:
20
+
21
+ ```bash
22
+ curl -X POST https://cs.example.com/api/upload \
23
+ -H "Authorization: Bearer $API_KEY" \
24
+ -F "file=@clip.mp4"
25
+ ```
26
+
27
+ Video limit is 100 MB; image limit is 10 MB. Stories are portrait-first — 1080×1920 is the intended shape, and it is what the story viewer lays out for.
28
+
29
+ ## Create
30
+
31
+ ```bash
32
+ curl -X POST https://cs.example.com/api/stories \
33
+ -H "Authorization: Bearer $API_KEY" \
34
+ -H "Content-Type: application/json" \
35
+ -d '{
36
+ "type": "video",
37
+ "media": {
38
+ "url": "https://…/media/video/abc.mp4",
39
+ "width": 1080,
40
+ "height": 1920,
41
+ "duration": 18
42
+ }
43
+ }'
44
+ ```
45
+
46
+ `type` is `image` or `video`. `media` accepts **either** a single object or an array — an array creates **one story per item** and returns the first, so if you post three clips you get three stories and must list to find the other two.
47
+
48
+ You can also set counters at creation time, which is useful when backfilling or seeding:
49
+
50
+ ```json
51
+ { "type": "image", "media": { "url": "…" }, "views": 120, "likes": 9 }
52
+ ```
53
+
54
+ ## Read
55
+
56
+ ```bash
57
+ GET /api/stories?page=1&limit=20
58
+ GET /api/stories?type=count # { "total": n }
59
+ GET /api/stories?kind=image # filter by media kind
60
+ GET /api/stories?kind=video
61
+ GET /api/stories?search=keyword
62
+ GET /api/stories/{story_id}
63
+ ```
64
+
65
+ Note the asymmetry: the **count** parameter is `type=count`, but the **media filter** is `kind=image` / `kind=video`. Using `type=image` for the filter does nothing.
66
+
67
+ ## Views and likes
68
+
69
+ ```bash
70
+ # increment
71
+ curl -X POST "https://cs.example.com/api/stories/$ID?action=view"
72
+
73
+ # like
74
+ curl -X POST "https://cs.example.com/api/stories/$ID?action=like&direction=inc"
75
+ curl -X POST "https://cs.example.com/api/stories/$ID?action=like&direction=dec"
76
+ ```
77
+
78
+ `action=view` is public — it is what the player fires. `direction` is `inc` or `dec`; there is no toggle, so a double-tap from a client that sends `inc` twice over-counts. If you are wiring a reaction UI, read the current `likes` first and send the direction that moves it the way you want.
79
+
80
+ ## Update
81
+
82
+ ```bash
83
+ curl -X PATCH https://cs.example.com/api/stories/$ID \
84
+ -H "Authorization: Bearer $API_KEY" \
85
+ -H "Content-Type: application/json" \
86
+ -d '{"views": 500, "media": {"url": "https://…/new.mp4", "width":1080, "height":1920}}'
87
+ ```
88
+
89
+ Partial update over `type`, `views`, `likes`, `media`.
90
+
91
+ ## Delete
92
+
93
+ ```bash
94
+ curl -X DELETE https://cs.example.com/api/stories/$ID \
95
+ -H "Authorization: Bearer $API_KEY"
96
+ ```
97
+
98
+ Permanent.
99
+
100
+ ## Via MCP
101
+
102
+ | Tool | Auth | Purpose |
103
+ |---|---|---|
104
+ | `list_stories` | public | Paginated, searchable |
105
+ | `get_story` | public | One story |
106
+ | `count_stories` | public | Total |
107
+ | `create_story` | API key | Create from a media URL |
108
+ | `update_story` | API key | Media, views, likes |
109
+ | `delete_story` | API key | Remove |
110
+
111
+ `ds-cs mcp` prints ready-to-paste config for every major client.
112
+
113
+ ## Mistakes that cost time
114
+
115
+ - Filtering with `type=image` instead of `kind=image` and getting the full list back.
116
+ - Posting an array and assuming one story was created — you created one per item.
117
+ - Sending `action=like` with no `direction`. It is `inc` or `dec`, never a toggle.
118
+ - Attaching an image URL to a story without uploading it first.
119
+ - Expecting a `content` caption field. Stories do not have one.
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: mcp-connect
3
+ description: Connect an AI agent to a DS-CS channel over MCP — remote HTTP or local stdio — and choose the right transport for the situation. Use when setting up Claude Desktop, Cursor, Claude Code, OpenCode or Windsurf against a channel, when MCP tools are not appearing, or when an agent needs read or write access to a channel's posts and stories.
4
+ ---
5
+
6
+ # Connecting an agent over MCP
7
+
8
+ ## Why this exists
9
+
10
+ Without MCP, an agent has to read docs, guess URLs, and hand-build HTTP requests — it works, and it gets the auth header wrong. With MCP the agent asks the server what tools exist and gets typed schemas. For anything an agent does repeatedly against a channel, wire MCP rather than teaching it `curl`.
11
+
12
+ ## Two transports
13
+
14
+ **Remote (HTTP)** — the channel serves MCP at `/api/mcp`. Any client connects over the network. No local process, works for clients on other machines, survives the channel moving.
15
+
16
+ **Local (stdio)** — `bun run mcp` runs a stdio server that talks straight to the database. Faster and works offline, but requires a local clone with `DATABASE_URL` and `API_KEY` set. Useful in development; not what a user should be running.
17
+
18
+ ## Let the CLI write the config
19
+
20
+ ```bash
21
+ ds-cs login --url https://cs.example.com --api-key "$API_KEY"
22
+ ds-cs mcp # print every format
23
+ ds-cs mcp claude-code # write .mcp.json
24
+ ds-cs mcp opencode # write opencode.json
25
+ ds-cs mcp cursor # write .cursor/mcp.json
26
+ ```
27
+
28
+ `ds-cs init --mcp` does the same during project setup. An existing config file is never overwritten — the CLI prints the snippet and leaves the merge to you, because clobbering a working MCP config to add one server is a bad trade.
29
+
30
+ ## The config
31
+
32
+ **Remote with write access:**
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "ds-cs": {
38
+ "url": "https://cs.example.com/api/mcp",
39
+ "headers": { "Authorization": "Bearer YOUR_API_KEY" }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ **Local:**
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "ds-cs": { "command": "bun", "args": ["run", "mcp"], "cwd": "/path/to/ds-cs" }
51
+ }
52
+ }
53
+ ```
54
+
55
+ Dropping the `headers` block leaves read-only access — which works, because reads are public by design.
56
+
57
+ ## Tools
58
+
59
+ **Read (no auth):** `list_posts`, `get_post`, `list_stories`, `get_story`, `count_stories`, `get_resume_config`
60
+
61
+ **Write (API key):** `create_post`, `update_post`, `delete_post`, `create_story`, `update_story`, `delete_story`, `update_resume_config`, `set_resume_enabled`
62
+
63
+ ## Auth
64
+
65
+ Read tools work without a key. Write tools require `Authorization: Bearer <API_KEY>`, where the key is the channel's `API_KEY` from `.env`. The remote HTTP server always uses `API_KEY`; the stdio server prefers `MCP_API_KEY` and falls back to `API_KEY` — that separation lets you give a local session a different key than production.
66
+
67
+ Missing key returns:
68
+
69
+ ```
70
+ Write access denied. Connect with an Authorization: Bearer <API_KEY> header.
71
+ ```
72
+
73
+ ## Verify it works
74
+
75
+ ```bash
76
+ curl https://cs.example.com/.well-known/mcp
77
+ ```
78
+
79
+ Then, over the wire:
80
+
81
+ ```bash
82
+ curl -X POST https://cs.example.com/api/mcp \
83
+ -H "Content-Type: application/json" \
84
+ -H "Accept: application/json, text/event-stream" \
85
+ -D headers.txt \
86
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'
87
+
88
+ SESSION_ID=$(grep -i 'mcp-session-id:' headers.txt | tr -d '\r' | awk '{print $2}')
89
+
90
+ curl -X POST https://cs.example.com/api/mcp \
91
+ -H "Content-Type: application/json" \
92
+ -H "Accept: application/json, text/event-stream" \
93
+ -H "Mcp-Session-Id: $SESSION_ID" \
94
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
95
+ ```
96
+
97
+ `tools/list` returning a populated array is the real test — clients vary in whether they call it automatically.
98
+
99
+ Interactive docs are at `https://cs.example.com/docs`, where every tool's schema can be exercised in the browser.
100
+
101
+ ## Troubleshooting
102
+
103
+ | Problem | Fix |
104
+ |---|---|
105
+ | Server not listed | Restart the client after editing config; check JSON syntax |
106
+ | "Session not found" | Re-initialize. Sessions expire after an hour idle |
107
+ | "Missing Mcp-Session-Id" | Use the Streamable HTTP transport, not SSE |
108
+ | Tools empty | Call `tools/list` after `initialize` |
109
+ | "Write access denied" | Add the `Authorization` header |
110
+ | Connection refused (local) | `bun run mcp` must be running, `DATABASE_URL` reachable |
111
+
112
+ Sessions are pruned after an hour of inactivity, and each session gets its own server instance. CORS is enabled for browser-based clients.
113
+
114
+ ## Beyond the tools
115
+
116
+ Connecting MCP does not give an agent product judgement — it gives it typed access. Anything about *how* to operate the channel (media limits, what a short is, when to pin) belongs in a skill, installed with `ds-cs add`. The combination is the point: tools for capability, skills for conduct.
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: post-with-media
3
+ description: Create, read, edit, pin and delete posts on a live DS-CS channel over the REST API — including the two-step media upload and the exact request shapes. Use when an agent needs to publish content, list or search existing posts, pin a post, or clean one up. For stories use manage-stories; for realtime delivery use webhooks.
4
+ ---
5
+
6
+ # Working with posts
7
+
8
+ ## Connection
9
+
10
+ ```
11
+ Base URL: https://<your-channel>
12
+ Auth: Authorization: Bearer <API_KEY> (write operations only)
13
+ ```
14
+
15
+ The API key is the `API_KEY` from the channel's `.env` (`openssl rand -hex 32` to generate one). **Reads are public — no key needed.** Writes (POST, PATCH, DELETE) require it, and are rate limited per IP and path (`API_KEY_RATE_LIMIT_MAX_REQUESTS`, default 30 per `API_KEY_RATE_LIMIT_WINDOW_MS`, default 60s).
16
+
17
+ Set it up once:
18
+
19
+ ```bash
20
+ ds-cs login --url https://cs.example.com --api-key "$API_KEY"
21
+ ```
22
+
23
+ ## Upload media first — always
24
+
25
+ A post references media **by URL**. There is no inline base64 body. Upload, then reference the returned `url`.
26
+
27
+ **Server-side (curl, agents, MCP — simplest):**
28
+
29
+ ```bash
30
+ curl -X POST https://cs.example.com/api/upload \
31
+ -H "Authorization: Bearer $API_KEY" \
32
+ -F "file=@photo.jpg"
33
+ ```
34
+
35
+ Returns:
36
+
37
+ ```json
38
+ {
39
+ "url": "https://…/media/image/9f8e….jpg",
40
+ "key": "media/image/9f8e….jpg",
41
+ "kind": "image",
42
+ "mimeType": "image/jpeg",
43
+ "size": 1234567,
44
+ "width": 1080,
45
+ "height": 1920,
46
+ "duration": 0,
47
+ "filename": "photo.jpg"
48
+ }
49
+ ```
50
+
51
+ **Direct browser-to-S3 (real upload progress):** `POST /api/upload/presign` with `{ filename, mimeType }` → `PUT` the file to the returned `uploadUrl` → `POST /api/upload/confirm` with `{ key, mimeType, filename, size, width, height, duration }` → the full media object. Prefer presign for large videos; the single-file endpoint buffers the whole thing in memory on the server.
52
+
53
+ ### Constraints
54
+
55
+ | Kind | Max size | Allowed MIME |
56
+ |---|---|---|
57
+ | image | 10 MB | jpeg, png, webp, gif, avif, svg+xml |
58
+ | video | 100 MB | mp4, webm, quicktime |
59
+ | audio | 25 MB | mpeg, mp4, wav, ogg, aac, flac |
60
+ | file | 25 MB | pdf, text/plain |
61
+
62
+ Override with `MEDIA_MAX_IMAGE_SIZE_MB`, `MEDIA_MAX_VIDEO_SIZE_MB`, `MEDIA_MAX_AUDIO_SIZE_MB`, `MEDIA_MAX_FILE_SIZE_MB`, `MEDIA_ALLOWED_MIME_TYPES`. Validation failures come back as `MediaValidationError` with a specific message and status.
63
+
64
+ ## Create a text post
65
+
66
+ ```bash
67
+ curl -X POST https://cs.example.com/api/posts \
68
+ -H "Authorization: Bearer $API_KEY" \
69
+ -H "Content-Type: application/json" \
70
+ -d '{"content":"Just shipped a new feature!"}'
71
+ ```
72
+
73
+ Returns `201` with the created post.
74
+
75
+ ## Create a media post
76
+
77
+ ```bash
78
+ curl -X POST https://cs.example.com/api/posts \
79
+ -H "Authorization: Bearer $API_KEY" \
80
+ -H "Content-Type: application/json" \
81
+ -d '{
82
+ "content": "Studio clip",
83
+ "media": [
84
+ {
85
+ "url": "https://…/media/video/abc.mp4",
86
+ "type": "video",
87
+ "width": 1080,
88
+ "height": 1920,
89
+ "duration": 42
90
+ }
91
+ ]
92
+ }'
93
+ ```
94
+
95
+ `type` is one of `text`, `image`, `video`, `voice`, `file`. `width`/`height` matter — the feed classifies a video as a **short** by aspect ratio (9:16, filtered to 1.6–2.0), so a vertical video with no dimensions is never treated as a short.
96
+
97
+ **Multiple media items create one post per item**, except when every item is an image, which produces a single carousel post. The response is the first post; use `list_posts` to find the others.
98
+
99
+ ## Read posts
100
+
101
+ ```bash
102
+ GET /api/posts?page=1&limit=20 # paginated
103
+ GET /api/posts?type=count # { "total": n }
104
+ GET /api/posts?type=shorts # vertical videos only
105
+ GET /api/posts?search=photography # free-text
106
+ GET /api/posts/{post_id} # one post, with reactions
107
+ ```
108
+
109
+ `limit` is capped at 100. List response shape:
110
+
111
+ ```json
112
+ {
113
+ "items": [
114
+ {
115
+ "id": "b1a2c3d4-…",
116
+ "type": "list",
117
+ "content": "…",
118
+ "media": [{ "kind": "image", "url": "…", "width": 1080, "height": 1920 }],
119
+ "reactions": [{ "emoji": "❤️", "count": 12 }],
120
+ "views": 342,
121
+ "pinnedAt": null,
122
+ "postStatus": "sent",
123
+ "createdAt": "2026-08-23T10:15:00.000Z"
124
+ }
125
+ ],
126
+ "page": 1,
127
+ "limit": 20,
128
+ "total": 1,
129
+ "hasMore": false
130
+ }
131
+ ```
132
+
133
+ `views` is a display counter stored as a string — treat it as opaque and do not parse it as a number.
134
+
135
+ ## Edit and pin
136
+
137
+ ```bash
138
+ curl -X PATCH https://cs.example.com/api/posts/$POST_ID \
139
+ -H "Authorization: Bearer $API_KEY" \
140
+ -H "Content-Type: application/json" \
141
+ -d '{"pinnedAt":"2026-08-21T00:00:00Z"}'
142
+ ```
143
+
144
+ Partial update. Any of `content`, `views`, `pinnedAt`, `media`, `type`. Passing `pinnedAt: null` unpins. Pinned posts surface in a dedicated bar and at the top of the feed.
145
+
146
+ ## React and view
147
+
148
+ ```bash
149
+ curl -X POST https://cs.example.com/api/posts/$POST_ID?action=reaction \
150
+ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
151
+ -d '{"emoji":"🔥"}'
152
+
153
+ curl -X POST https://cs.example.com/api/posts/$POST_ID?action=view
154
+ ```
155
+
156
+ `action=view` increments the counter and is public.
157
+
158
+ ## Delete
159
+
160
+ ```bash
161
+ curl -X DELETE https://cs.example.com/api/posts/$POST_ID \
162
+ -H "Authorization: Bearer $API_KEY"
163
+ ```
164
+
165
+ Permanent. There is no trash and no undo.
166
+
167
+ ## Via MCP instead
168
+
169
+ If an MCP client is connected, prefer the typed tools — the server enforces auth and the schemas for you:
170
+
171
+ | Tool | Auth | Purpose |
172
+ |---|---|---|
173
+ | `list_posts` | public | Paginated, searchable |
174
+ | `get_post` | public | One post with reactions |
175
+ | `create_post` | API key | Text or media post |
176
+ | `update_post` | API key | Content / pin status |
177
+ | `delete_post` | API key | Remove a post |
178
+
179
+ Connect with `ds-cs mcp claude-code`, or point any client at `https://<channel>/api/mcp`.
180
+
181
+ ## Verify
182
+
183
+ Discovery endpoint, useful for confirming a channel is alive:
184
+
185
+ ```bash
186
+ curl https://cs.example.com/.well-known/mcp
187
+ ```
188
+
189
+ Full REST reference is served at `https://cs.example.com/docs`, and the OpenAPI spec at `/openapi.json`.
190
+
191
+ ## Mistakes that cost time
192
+
193
+ - Posting a URL that was never uploaded — the media silently renders broken.
194
+ - Omitting `width`/`height` on a vertical video and wondering why it is not a short.
195
+ - Expecting `update_post` to replace media — it is a partial patch; send the fields you mean to change.
196
+ - Deleting by a guessed ID. `list_posts` first, then delete the exact `id` that came back.
197
+ - Assuming reads need auth. They do not, and a 401 on a `GET` means your *key* is wrong, not that auth is required.