@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.
- package/README.md +134 -2
- package/bin/ds-cs.mjs +14 -0
- package/package.json +33 -4
- package/skills/README.md +71 -0
- package/skills/dev/add-component/SKILL.md +166 -0
- package/skills/dev/add-orpc-endpoint/SKILL.md +143 -0
- package/skills/dev/db-migration/SKILL.md +104 -0
- package/skills/dev/release/SKILL.md +106 -0
- package/skills/product/brand-config/SKILL.md +98 -0
- package/skills/product/manage-stories/SKILL.md +119 -0
- package/skills/product/mcp-connect/SKILL.md +116 -0
- package/skills/product/post-with-media/SKILL.md +197 -0
- package/skills/product/self-host-deploy/SKILL.md +112 -0
- package/skills/product/webhooks/SKILL.md +160 -0
- package/src/agents.mjs +130 -0
- package/src/args.mjs +111 -0
- package/src/auth.mjs +82 -0
- package/src/commands/add.mjs +118 -0
- package/src/commands/auth.mjs +87 -0
- package/src/commands/init.mjs +106 -0
- package/src/commands/list.mjs +89 -0
- package/src/commands/mcp.mjs +101 -0
- package/src/commands/remove.mjs +80 -0
- package/src/commands/search.mjs +51 -0
- package/src/commands/target.mjs +83 -0
- package/src/commands/update.mjs +94 -0
- package/src/index.mjs +176 -0
- package/src/install.mjs +174 -0
- package/src/lockfile.mjs +74 -0
- package/src/mcp-config.mjs +39 -0
- package/src/registry/archive.mjs +209 -0
- package/src/registry/fetch.mjs +114 -0
- package/src/registry/sources.mjs +395 -0
- package/src/skills/discover.mjs +170 -0
- package/src/skills/frontmatter.mjs +85 -0
- package/src/utils/errors.mjs +13 -0
- package/src/utils/fs.mjs +106 -0
- package/src/utils/log.mjs +85 -0
- 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.
|