Compare commits

..

195 commits

Author SHA1 Message Date
Nevo David
09088a5391 Merge remote-tracking branch 'origin/main'
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
Code Quality Analysis / Analyze (javascript-typescript) (push) Has been cancelled
2026-05-18 20:49:30 +07:00
Nevo David
faeb89853b feat: threads error 2026-05-18 20:32:45 +07:00
Nevo David
6fc51da7e3
Merge pull request #1538 from gitroomhq/feat/list-view-post-filters
Add state filter (all/scheduled/draft/published) to list view
2026-05-18 20:24:39 +07:00
Santosh Bhandari
415c9c4ba8
Merge pull request #1537 from gitroomhq/tiktok-info
Show TikTok title/content restriction notice for video posts
2026-05-18 10:29:12 +00:00
Santosh Bhandari
e19c855da6
Merge pull request #1539 from gitroomhq/fix/tiktok-pending-share-error-message
Clarify TikTok pending-share error mentions the 24-hour window
2026-05-18 10:27:59 +00:00
Santosh Bhandari
7e0bb7075e fix: clarify TikTok pending-share error mentions 24-hour window
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
TikTok's spam_risk_too_many_pending_share limit applies per 24-hour
period; the previous message did not state the window.
2026-05-18 15:28:03 +05:45
Nevo David
0b3328daeb feat: pinterest fixes 2026-05-18 16:39:38 +07:00
Santosh Bhandari
4811741e63 feat: filter list view by post state (all/scheduled/draft/published)
Adds a state filter to the calendar list view so users can see all
posts (default) or narrow to scheduled, draft, or published. The
backend repository now switches its WHERE/orderBy off the new query
param; 'all' includes ERROR posts so failed publishes remain visible.
2026-05-18 15:21:28 +05:45
Santosh Bhandari
7dda2812d7 feat: add TikTok restriction notice for video posts
Show an inline warning explaining title/content limitations for
direct-post vs upload-only video modes on TikTok.
2026-05-18 14:44:25 +05:45
Nevo David
2316a45388 feat: upgrade nextjs due to security risks 2026-05-18 13:55:12 +07:00
Nevo David
38b0ac8c70 feat: update nestjs 2026-05-15 16:15:50 +07:00
Nevo David
17fa64726c feat: tracking 2026-05-14 18:10:01 +07:00
Nevo David
03ddef66e2 feat: trial tracker 2026-05-14 17:43:42 +07:00
Nevo David
0dce16029e feat: google tag 2026-05-14 17:32:54 +07:00
Nevo David
03aa6b13dd Merge remote-tracking branch 'origin/main' 2026-05-14 16:51:27 +07:00
Nevo David
630602858e feat: gtm 2026-05-14 16:50:01 +07:00
Nevo David
715d3e40fd
Merge pull request #1514 from gitroomhq/feat/creation-method-tracking
feat: track post creation method (WEB/API/MCP/AUTOPOST)
2026-05-14 12:40:16 +07:00
Santosh Bhandari
e63d6d2cf2 feat: restrict public API creation methods to CLI and API 2026-05-14 10:06:58 +05:45
Nevo David
f2ebadab9e
Merge pull request #1515 from gitroomhq/feat/has-extension-helper
feat: hasExtension helper for media type detection
2026-05-14 11:13:36 +07:00
Nevo David
1677714670
Merge branch 'main' into feat/has-extension-helper 2026-05-14 11:13:12 +07:00
Enno Gelhaus
b4635f026b
feat: increase default api rate limit to 90 2026-05-13 17:43:22 +02:00
Nevo David
5f2f5581b2 feat: 3d secure fix 2026-05-13 19:53:53 +07:00
Nevo David
7cc3d9bd78 feat: stripe fix 2026-05-13 09:01:22 +07:00
Nevo David
d2c1eabc8b feat: fix workflow after sleep 2026-05-12 23:57:13 +07:00
Nevo David
4ee5231cb2 Merge remote-tracking branch 'origin/main' 2026-05-12 23:34:02 +07:00
Nevo David
16abf0dc9a feat: posting if no subscription 2026-05-12 23:33:49 +07:00
Enno Gelhaus
86368d7b7b
feat: upgrade ci pnpm 2026-05-12 15:13:56 +02:00
Nevo David
e986d9e493 feat: strip links 2026-05-12 19:51:23 +07:00
Santosh Bhandari
aa0c16b648 feat: accept CLI creation method via public API 2026-05-12 17:25:10 +05:45
Santosh Bhandari
510f396389 feat: show creation method badge only when impersonating 2026-05-12 17:06:29 +05:45
Santosh Bhandari
80b6bdcabe feat: hasExtension helper for media type detection 2026-05-12 16:23:11 +05:45
Santosh Bhandari
e153ab0a9b feat: track post creation method (WEB/API/MCP/AUTOPOST) 2026-05-12 14:41:59 +05:45
Nevo David
cf0ab36a23 feat: deletedAt integrations ignore 2026-05-12 13:27:58 +07:00
Nevo David
009bd36528 feat: fix tiktok url ownership error 2026-05-11 17:50:14 +07:00
Nevo David
905392513f feat: no post 2026-05-11 14:16:08 +07:00
Nevo David
7be292094a feat: no post 2026-05-11 14:15:33 +07:00
Nevo David
6e55eb3b92 feat: more cover for pending queues 2026-05-11 13:57:46 +07:00
Nevo David
39f2a176e1 Merge remote-tracking branch 'origin/main' 2026-05-11 11:19:41 +07:00
Nevo David
638b071283 feat: corrupted file 2026-05-11 11:19:02 +07:00
Nevo David
060c77a68c
Merge pull request #1488 from gitroomhq/fix/ui-modal-max-width
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
Code Quality Analysis / Analyze (javascript-typescript) (push) Has been cancelled
ui: update the modal such that for long text won't cause overflow
2026-05-07 10:40:39 +07:00
Nevo David
d4405906bd
Merge pull request #1489 from gitroomhq/feat/ui-notification-time
feat: update notification list to be scrollable and added time
2026-05-07 10:40:21 +07:00
Nevo David
c8f1074f48
Merge pull request #1494 from gitroomhq/fix/registration-email-lowercase
fix: lowercase email on local registration
2026-05-06 12:10:52 +07:00
Santosh Bhandari
dcb1b0188a fix: lowercase email on local registration 2026-05-06 10:46:25 +05:45
Enno Gelhaus
22f436e72e
feat: simplify pr template 2026-05-04 21:00:24 +02:00
Enno Gelhaus
53f0967e67
feat: merge queue 2026-05-04 15:50:13 +02:00
Santosh Bhandari
18a1a80871 feat: update notification list to be scrollable and added time 2026-05-04 19:05:21 +05:45
Santosh Bhandari
a6967c8519 ui: update the modal such that for long text won't cause overflow 2026-05-04 18:41:34 +05:45
Enno Gelhaus
7e92764ad2
Merge pull request #1482 from gitroomhq/feat/contribute-checker
feat: contributor form
2026-05-04 10:29:58 +02:00
Enno Gelhaus
1bf32426c7
Update contribution form link in CONTRIBUTING.md 2026-05-04 10:29:09 +02:00
Enno Gelhaus
779764aa5d
Fix contribution form link in PR template
Updated contribution form link in PR template.
2026-05-04 10:28:48 +02:00
Nevo David
232ebb2528
Merge pull request #1483 from gitroomhq/fix/linkedin-gif-sharp-compress-removal
fix: remove processing GIF via sharp in linkedin
2026-05-04 15:03:25 +07:00
Santosh Bhandari
d056225053 fix: remove processing GIF via sharp in linkedin 2026-05-04 08:09:25 +05:45
Enno Gelhaus
e419e05f09
Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-03 16:27:56 +02:00
Enno Gelhaus
47ce014204
feat: contributor form 2026-05-03 14:17:54 +02:00
Nevo David
9d14b0262d Merge remote-tracking branch 'origin/main' 2026-05-02 14:00:32 +07:00
Nevo David
971042a074 feat: shrink workflow payload 2026-05-02 14:00:09 +07:00
Nevo David
c3976e554f
Merge pull request #1479 from gitroomhq/fix/discord-provider-error-handler
fix: properly handle error in discord provider
2026-05-01 09:42:49 +07:00
Santosh Bhandari
ef111eb1c4 fix: update error message 2026-04-30 21:45:25 +05:45
Enno Gelhaus
3ee35a7348
feat: temporarily disable cache to fix build 2026-04-30 17:52:36 +02:00
Santosh Bhandari
d6bc6eb0ff fix: properly handle error in discord provider 2026-04-30 21:28:11 +05:45
Nevo David
0d98fc02fb feat: X errors and force upload
Some checks failed
Build / build (22.12.0) (push) Has been cancelled
Code Quality Analysis / Analyze (javascript-typescript) (push) Has been cancelled
2026-04-30 18:34:18 +07:00
Nevo David
7264c00298 feat: err 2026-04-30 17:44:06 +07:00
Nevo David
bb7cd46a4f feat: errors 2026-04-30 17:24:01 +07:00
Nevo David
7236213ea4 feat: fix xss
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-04-27 14:50:55 +07:00
Nevo David
4bdcfec3d7 Merge remote-tracking branch 'origin/main' 2026-04-27 13:36:56 +07:00
Nevo David
cdcf63bf6b feat: when creating a postiz app, allow localhost 2026-04-27 13:17:12 +07:00
Enno Gelhaus
fd6553196b
Merge pull request #1465 from mousaa/pr/x-url-env
feat: add X_URL env var to docker-compose and .env.example
2026-04-26 14:49:40 +02:00
Nevo David
90b2581048 feat: better information on use Postiz for developers 2026-04-26 15:04:01 +07:00
Nevo David
4e7864c929 feat: better MCP options 2026-04-26 14:58:19 +07:00
Ahmed Mousa
b91ffdc9c3 fix: remove duplicate X_URL entry in docker-compose
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 16:17:55 -04:00
Ahmed Mousa
d75662b56a feat: add X_URL to docker-compose and .env.example
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 16:13:58 -04:00
Nevo David
876be6f8c6 Merge remote-tracking branch 'origin/main' 2026-04-22 22:26:50 +07:00
Nevo David
071143dcb0 feat: security fix 2026-04-22 22:25:48 +07:00
Enno Gelhaus
da448012dd
feat: remove insecure & unnecessary workflow. 2026-04-22 16:20:52 +02:00
Nevo David
e51cae1614 feat: fix analytics 2026-04-22 13:09:39 +07:00
Nevo David
fa5d7f4c40 feat: fix pop 2026-04-22 11:09:42 +07:00
Nevo David
0b554e6844 Merge remote-tracking branch 'origin/main' 2026-04-20 12:27:13 +07:00
Nevo David
8951289426 feat: fix previous agent messages 2026-04-20 12:26:56 +07:00
Enno Gelhaus
ec4759e934
feat: include cloud in security scope 2026-04-20 03:11:24 +02:00
Enno Gelhaus
c61e061145
feat: security refinement 2026-04-19 23:09:01 +02:00
Enno Gelhaus
8cfb634b66
feat: security rewording 2026-04-19 23:00:16 +02:00
Enno Gelhaus
55a542485a
feat: security additions 2026-04-19 22:58:05 +02:00
Enno Gelhaus
0eddfb3304
feat: security policy changes
- Add Response Timelines
- Add Security Scope
- Modify maintainer list for security maintainers
2026-04-19 21:13:12 +02:00
Nevo David
88006a7614 feat: mobile app iframe 2026-04-19 13:48:15 +07:00
Nevo David
45e55c545d feat: add auth header 2026-04-18 19:53:19 +07:00
Nevo David
0a8fa5bff6 feat: fix bridge 2026-04-18 18:36:57 +07:00
Nevo David
65d23707ab feat: fix bridge 2026-04-18 18:20:18 +07:00
Nevo David
846954f059 feat: bridge fix 2026-04-18 18:00:22 +07:00
Nevo David
c79965718f feat: auth changes 2026-04-18 17:14:10 +07:00
Nevo David
027c9caa96 feat: fix check validity 2026-04-18 02:18:28 +07:00
Nevo David
8a7e8eb8f3 feat: empty object 2026-04-18 01:16:32 +07:00
Nevo David
71b2e2e793 feat: fix validity 2026-04-18 00:17:05 +07:00
Nevo David
2ae293916b feat: fix validity 2026-04-18 00:12:41 +07:00
Nevo David
e5947034ab feat: check validity 2026-04-17 23:53:29 +07:00
Nevo David
0ecca5298d feat: remove gap 2026-04-17 22:41:30 +07:00
Nevo David
7d9b99abf3 feat: provider edit preview 2026-04-17 21:55:47 +07:00
Nevo David
45bdf128e9 feat: oauth mobile callback 2026-04-17 18:02:54 +07:00
Nevo David
4e277ed32d feat: load first 2026-04-17 17:52:15 +07:00
Nevo David
5257f2fabe feat: redirect url 2026-04-17 17:34:34 +07:00
Nevo David
386fc7b049 feat: fix generation 2026-04-15 17:53:37 +07:00
Nevo David
ec8c0f6fb9 feat: search in media 2026-04-14 20:35:17 +07:00
Nevo David
eb0334d96d Merge remote-tracking branch 'origin/main' 2026-04-13 23:31:47 +07:00
Nevo David
288a4d428b feat: change post status 2026-04-13 23:31:08 +07:00
Enno Gelhaus
1145e51ea6
feat: sentry replay 2026-04-13 07:10:56 +02:00
Nevo David
3ea302202d feat: fix advisory
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-04-12 10:27:01 +07:00
Nevo David
e3b3b82fae feat: instagram better error 2026-04-10 20:17:38 +07:00
Nevo David
318e9da8d1 feat: refresh token, show error 2026-04-10 19:08:09 +07:00
Nevo David
186d3370e9 feat: instagram refresh fix 2026-04-10 18:33:19 +07:00
Nevo David
98c32c3da5 feat: subreddit posting fix with api 2026-04-10 16:27:34 +07:00
Nevo David
3e8a0ab817 feat: langchain upgrade
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-04-09 18:28:44 +07:00
Nevo David
6a06b210c2 Merge remote-tracking branch 'origin/main' 2026-04-09 18:11:21 +07:00
Nevo David
30e8b77709 feat: security fixes 2026-04-09 18:11:07 +07:00
Enno Gelhaus
59e535e258
fix: pr quality 2026-04-08 12:25:49 +02:00
Enno Gelhaus
a8918a6284
feat: update pr quality 2026-04-08 12:24:30 +02:00
Nevo David
26d7ffa0ed Merge remote-tracking branch 'origin/main' 2026-04-07 18:05:44 +07:00
Nevo David
c7867ab05e feat: download invoices 2026-04-07 18:05:30 +07:00
Nevo David
9b50a18f2d
Merge pull request #1383 from swaraj017/debug/youtube-refresh-token
fix(youtube): preserve existing refresh token to avoid daily re-auth
2026-04-07 10:57:28 +07:00
swaraj017
943293dc7d fix(youtube): preserve existing refresh token to avoid daily re-auth 2026-04-06 23:33:26 +05:30
Enno Gelhaus
61a563a06c
Merge pull request #1369 from fizikiukas/patch-1
Update Hostinger link in README.md
2026-04-04 13:14:39 +02:00
Enno Gelhaus
76676510f2
Merge branch 'main' of https://github.com/gitroomhq/postiz-app 2026-04-04 12:46:54 +02:00
Enno Gelhaus
af8d04002c
feat: gitignore 2026-04-04 12:46:50 +02:00
Nevo David
d16945a9e2 feat: fix cli + server openai token 2026-04-04 15:00:27 +07:00
Nevo David
fb43544bb2 feat: fix parseR 2026-04-04 12:56:05 +07:00
Nevo David
008deb0daf feat: update axios version 2026-04-04 12:05:11 +07:00
Nevo David
c5882d3eae feat: auto accept and json response 2026-04-03 22:52:02 +07:00
Nevo David
1bbfa8d603 feat: fix header for all agents 2026-04-03 22:25:24 +07:00
Nevo David
6bf80ff846 feat: upgrade mcp 2026-04-03 21:48:41 +07:00
Nevo David
f55cca519c feat: override 2026-04-03 19:39:29 +07:00
Nevo David
ac109bf564 feat: mcp with oauth 2026-04-03 19:17:19 +07:00
Nevo David
5d46a6cd34 feat: new mastra 2026-04-03 18:39:33 +07:00
Nevo David
507a006b9f Merge remote-tracking branch 'origin/main' 2026-04-03 13:34:36 +07:00
Nevo David
073479bc4a feat: pinterest, show more boards 2026-04-03 13:34:24 +07:00
Valentinas Čirba
687925bcbf
Update Hostinger link in README.md 2026-04-03 08:35:00 +03:00
Nevo David
2d434fe198
Merge pull request #1359 from gitroomhq/feat/orchestrator-sentry
Feat/orchestrator sentry
2026-04-02 14:33:13 +07:00
Enno Gelhaus
e97be93dc4
revert: add sentry to orchestrator appmodule 2026-04-01 20:40:53 +02:00
Enno Gelhaus
bb63a7c8a9
feat: add sentry to orchestrator appmodule 2026-04-01 20:34:57 +02:00
Enno Gelhaus
0f4db4a375
feat: add sentry in orchestrator 2026-04-01 20:32:13 +02:00
Nevo David
3a376f4a9c feat: health check for temporal 2026-03-31 23:12:12 +07:00
Nevo David
afda06bb6a feat: show port 2026-03-31 21:36:37 +07:00
Nevo David
1840a6db6d feat: remove whitespace 2026-03-31 21:13:36 +07:00
Nevo David
b0832740d0 feat: developers UI, description 2026-03-31 13:42:44 +07:00
Nevo David
40d9d11f72 feat: add developers sections for non paying customers 2026-03-31 12:00:11 +07:00
Nevo David
7b67b4a6b5 Merge remote-tracking branch 'origin/main' 2026-03-31 11:38:09 +07:00
Nevo David
8c03078086 feat: fix integrations + import option + reelfarm 2026-03-31 11:37:51 +07:00
Enno Gelhaus
5ae4c950db
feat: secure /webhooks with IsSafeWebhookUrl
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-29 17:56:45 +02:00
Nevo David
6f7a80f689 feat: steam url fix
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-29 13:41:13 +07:00
Nevo David
52f59bcfde feat: steam url fix 2026-03-29 13:40:05 +07:00
Nevo David
13fedeca8b feat: upload from url, prevent internal access 2026-03-29 12:32:59 +07:00
Nevo David
f55253d1ab feat: refresh channel fix 2026-03-29 10:14:24 +07:00
Nevo David
e6a019b778 feat: announcement 2026-03-27 09:47:43 +07:00
Nevo David
8bb1935834 feat: upgrade polotno 2026-03-26 20:51:06 +07:00
Nevo David
117dab594e feat: fix tiktok key 2026-03-26 11:35:04 +07:00
Nevo David
777ca3d329 feat: fix typing 2026-03-26 11:21:11 +07:00
Nevo David
666de6ac06 feat: import post 2026-03-26 11:11:55 +07:00
Nevo David
2786fbaeba feat(x): add with AI, paid partenership 2026-03-26 09:48:55 +07:00
Nevo David
6aa5bf3591 fix mention component 2026-03-26 00:50:55 +07:00
Nevo David
d8386efc52 fix: mark posts with errors 2026-03-25 23:00:12 +07:00
Nevo David
be5d871896 fix: autopost url
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-25 17:45:18 +07:00
Nevo David
0ad89ccd26 feat: protect webhooks 2026-03-25 17:39:24 +07:00
Nevo David
92b82837e9 feat: remove Image
Some checks failed
Build Containers / build-containers-common (push) Has been cancelled
Build / build (22.12.0) (push) Has been cancelled
Build Containers / build-containers (amd64, ubuntu-latest) (push) Has been cancelled
Build Containers / build-containers (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build Containers / build-container-manifest (push) Has been cancelled
2026-03-25 16:51:13 +07:00
Nevo David
0c7000276d feat: change lang 2026-03-25 16:17:34 +07:00
Nevo David
a8c8428ae2 feat: fix direction 2026-03-25 16:08:30 +07:00
Nevo David
f675dd5179 feat: change dir 2026-03-25 15:57:46 +07:00
Nevo David
358bf62804 feat: layout 2026-03-25 15:17:35 +07:00
Nevo David
e621f965b5 feat: layout 2026-03-25 15:02:47 +07:00
Nevo David
072c6bac4a feat: fix language, dont cache cookies 2026-03-25 14:52:49 +07:00
Nevo David
c7a88b98bc feat: language switch 2026-03-25 14:42:00 +07:00
Nevo David
3bc3d004fa feat: language switch 2026-03-25 14:24:26 +07:00
Nevo David
afe06768fe feat: fix direction 2026-03-25 14:14:25 +07:00
Nevo David
41d48a81fd Merge remote-tracking branch 'origin/main' 2026-03-25 13:56:17 +07:00
Nevo David
b71297db57 feat: upgrade nextjs to the latest version 2026-03-25 13:55:45 +07:00
Enno Gelhaus
c280139917
feat: update security guidelines
- Add AI section
- Add PoC requirement
- Change reporting behavior
- Remove template
2026-03-23 18:31:23 +01:00
Nevo David
20053fc2f8
Remove Enterprise section from README
Removed the Postiz-as-a-service - Enterprise section from the README.
2026-03-22 13:32:31 +07:00
Nevo David
1aa47351a4 feat: revert all the mod changes 2026-03-22 10:23:52 +07:00
Nevo David
40c443ba96 feat: revert workflow| 2026-03-22 10:12:02 +07:00
Nevo David
cb8560e183 Merge remote-tracking branch 'origin/main' 2026-03-22 10:10:28 +07:00
Nevo David
9df6ab6b48 feat: tiktok 2026-03-22 10:10:14 +07:00
Enno Gelhaus
9a29793e2c
improve: sentry metrics 2026-03-21 23:30:20 +01:00
Enno Gelhaus
8d25914bd1
feat: sentry langchain 2026-03-21 23:07:42 +01:00
Enno Gelhaus
ac61be6454
fix: sentry metrics 2026-03-21 23:04:27 +01:00
Enno Gelhaus
8bbed0698d
Merge pull request #1331 from gitroomhq/feat/sentry-metrics
feat: a lot of sentry metrics
2026-03-21 22:34:22 +01:00
Enno Gelhaus
da995de88f
feat: security 2026-03-21 22:32:34 +01:00
Enno Gelhaus
1c04544c54
feat: a lot of sentry metrics 2026-03-21 22:18:28 +01:00
Enno Gelhaus
c539fa8cf4
feat: sentry v10.45 2026-03-21 14:46:29 +01:00
Nevo David
c164db76d9 Merge remote-tracking branch 'origin/main' 2026-03-21 16:32:48 +07:00
Nevo David
8dcfffff53 feat: added cus 2026-03-21 16:32:34 +07:00
Enno Gelhaus
b6d6b7283f
improve: PR Quality 2026-03-20 19:22:52 +01:00
Enno Gelhaus
33f3e552cb
fix: format in PR Quality 2026-03-20 19:16:55 +01:00
Enno Gelhaus
2f85198887
improve: PR Quality 2026-03-20 19:15:27 +01:00
Enno Gelhaus
2cd2911de2
Merge pull request #1326 from swaraj017/fix/redirect-register-to-login
fix: redirect /auth/register to /auth/login when registration is disabled
2026-03-20 16:27:10 +01:00
Enno Gelhaus
65939d3341
feat: improve PR Quality Workflow 2026-03-20 16:03:34 +01:00
Enno Gelhaus
eb14bd9f05
feat: Add AI section to PR Template 2026-03-20 15:58:25 +01:00
Enno Gelhaus
fa27e38ca0
feat: Add AI section to CONTRIBUTING.md 2026-03-20 15:57:17 +01:00
swaraj017
2f6896b332 fix: use correct DISABLE_REGISTRATION env variable 2026-03-18 14:59:17 +05:30
swaraj017
472ef851c2 fix: redirect /auth/register to /auth/login when registration is disabled 2026-03-18 12:45:43 +05:30
268 changed files with 13362 additions and 12338 deletions

View file

@ -40,6 +40,7 @@ STORAGE_PROVIDER="local"
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""
# Social Media API Settings
X_URL=""
X_API_KEY=""
X_API_SECRET=""
LINKEDIN_CLIENT_ID=""

View file

@ -1,3 +1,5 @@
<!-- Remember to first apply via [the contribution form](https://contribute.postiz.com/p/postiz) before submitting a PR. -->
# What kind of change does this PR introduce?
eg: Bug fix, feature, docs update, ...
@ -15,5 +17,6 @@ eg: Did you discuss this change with anybody before working on it (not required,
Put a "X" in the boxes below to indicate you have followed the checklist;
- [ ] I have read the [CONTRIBUTING](https://github.com/gitroomhq/postiz-app/blob/main/CONTRIBUTING.md) guide.
- [ ] I checked that there were not similar issues or PRs already open for this.
- [ ] This PR fixes just ONE issue (do not include multiple issues or types of change in the same PR) For example, don't try and fix a UI issue and include new dependencies in the same PR.
- [ ] I confirm I have not used AI to submit this PR or generate code for it.
- [ ] I checked that there were no similar issues or PRs already open for this.
- [ ] This PR fixes just ONE issue

View file

@ -3,6 +3,8 @@ name: Build
on:
push:
merge_group:
pull_request:
jobs:
build:
@ -27,7 +29,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
version: 10
run_install: false
- name: Get pnpm store directory
@ -35,15 +37,15 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: |
${{ env.STORE_PATH }}
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
# - name: Setup pnpm cache
# uses: actions/cache@v4
# with:
# path: |
# ${{ env.STORE_PATH }}
# ${{ github.workspace }}/.next/cache
# key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
# restore-keys: |
# ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Install dependencies
run: pnpm install

View file

@ -1,5 +1,5 @@
---
name: "Code Quality Analysis"
name: "Code Quality Analysis"
on:
push:
@ -9,6 +9,8 @@ on:
- apps/**
- '!apps/docs/**'
- libraries/**
merge_group:
jobs:
analyze:

View file

@ -1,38 +0,0 @@
name: Build and Publish PR Docker Image
on:
pull_request_target:
types: [opened, synchronize]
permissions: write-all
jobs:
build-and-publish:
runs-on: ubuntu-latest
environment:
name: build-pr
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set image tag
id: vars
run: echo "IMAGE_TAG=ghcr.io/gitroomhq/postiz-app-pr:${{ github.event.pull_request.number }}" >> $GITHUB_ENV
- name: Build Docker image from Dockerfile.dev
run: docker build -f Dockerfile.dev -t $IMAGE_TAG .
- name: Push Docker image to GHCR
run: docker push $IMAGE_TAG

View file

@ -1,18 +0,0 @@
name: PR Quality
permissions:
contents: read
issues: read
pull-requests: write
on:
pull_request_target:
types: [opened, reopened]
jobs:
anti-slop:
runs-on: ubuntu-latest
steps:
- uses: peakoss/anti-slop@v0
with:
max-failures: 4

5
.gitignore vendored
View file

@ -19,7 +19,7 @@ node_modules
.vscode/*
# IDE - VSCode
.vscode/*
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
@ -58,3 +58,6 @@ Thumbs.db
.secrets/
libraries/plugins/src/plugins.ts
i18n.cache
# Generated by apps/frontend/scripts/fetch-gtm.mjs on install
apps/frontend/public/g.js

View file

@ -6,6 +6,10 @@ Contributions are welcome - code, docs, whatever it might be! If this is your fi
The main documentation site has a [developer guide](https://docs.postiz.com/developer-guide) . That guide provides you a good understanding of the project structure, and how to setup your development environment. Read this document after you have read that guide. This document is intended to provide you a good understanding of how to submit your first contribution.
## Apply via the contribution form
To submit your contribution, please fill out the [contribution form](https://contribute.postiz.com/p/postiz). This helps us evaluate whether your contribution is a good fit for the project. We will review your submission and get back to you as soon as possible.
## Write code with others
This is an open source project, with an open and welcoming community that is always keen to welcome new contributors. We recommend the two best ways to interact with the community are:
@ -27,6 +31,11 @@ Contributions can include:
- **Feature requests:** Suggesting new capabilities or integrations.
- **Bug reports:** Identifying and reporting issues.
## AI
To ensure the quality and maintainability of the codebase, **we do not accept Pull Requests generated primarily by AI tools** (e.g., ChatGPT, GitHub Copilot, Claude Code, etc.).
All contributions must be the original work of the author. We reserve the right to close any PR that appears to be AI-generated without further review.
## How to contribute
This project follows a Fork/Feature Branch/Pull Request model. If you're not familiar with this, here's how it works:

View file

@ -65,20 +65,6 @@
<a href="https://apps.make.com/postiz">Make.com integration</a>
</p>
<br />
## New - Postiz-as-a-service - Enterprise (Cloud)
Integrate powerful social media scheduling capabilities into your SaaS. <br />Multi-tenant architecture designed for SaaS companies who want to offer social media management to their users.
- **Skip App Approvals** - Use Postiz apps directly without going through lengthy social platform approval processes. Get the full power of Postiz instantly.
- **Multi-Tenant Architecture** - each of your customers gets their own isolated environment with separate accounts, channels, and team management.
- **Headless API** - Full REST API access to build your own frontend experience. Complete control over the user interface and branding.
- **Full OAuth Support** - Connect all major social platforms including Facebook, Instagram, Twitter, LinkedIn, TikTok, and more.
[Check it here](https://postiz.com/enterprise)
<br /><br />
## 🔌 See the leading Postiz features
@ -99,7 +85,7 @@ Integrate powerful social media scheduling capabilities into your SaaS. <br />Mu
| Sponsor | Logo | Description |
|---------|:-----------------------------------------------------------------------:|-----------------|
| [Hostinger](https://www.hostinger.com/?ref=postiz) | <img src=".github/sponsors/hostinger.png" alt="Hostinger" width="500"/> | Hostinger is on a mission to make online success possible for anyone from developers to aspiring bloggers and business owners |
| [Hostinger](https://www.hostinger.com/vps/docker/postiz?ref=postiz) | <img src=".github/sponsors/hostinger.png" alt="Hostinger" width="500"/> | Hostinger is on a mission to make online success possible for anyone from developers to aspiring bloggers and business owners |
| [Virlo](https://dev.virlo.ai/?ref=postiz) | <img src="https://github.com/user-attachments/assets/25182598-5344-45fc-b9cd-e4cfa16aabfd" alt="Virlo" width="500"/> | Virlo is the #1 social media trend spotting and all-in-one GTM tool for teams leveraging short-form video |

View file

@ -4,26 +4,48 @@
The Postiz app is committed to ensuring the security and integrity of our users' data. This security policy outlines our procedures for handling security vulnerabilities and our disclosure policy.
## Reporting Security Vulnerabilities
## Scope
If you discover a security vulnerability in the Postiz app, please report it to us privately via email to one of the maintainers:
We, at Postiz (gitroomhq), cover the following scopes for vulnerability disclosures:
- @nevo-david
- @egelhaus ([E-Mail](mailto:egelhaus@ennogelhaus.de))
- The core repository for `postiz-app` (github.com/gitroomhq/postiz-app)
- All `gitroomhq` repositories that are official components, tooling, or integrations of Postiz
- Official Postiz container images published under `gitroomhq` on GHCR
- Official Postiz CLI tools and NPM packages (NPM org: @postiz)
- Postiz-Cloud related infrastructure & services. (API, Frontend, Configurations etc.)
- Plugins for Postiz maintained within the `gitroomhq` organization
When reporting a security vulnerability, please provide as much detail as possible, including:
- A clear description of the vulnerability
- Steps to reproduce the vulnerability
- Any relevant code or configuration files
Vulnerabilities in third-party dependencies or user-hosted infrastructure are outside of this scope.
## Supported Versions
This project currently only supports the latest release. We recommend that users always use the latest version of the Postiz app to ensure they have the latest security patches.
*CVE IDs will only be assigned to vulnerabilities affecting currently supported versions.*
## Reporting Security Vulnerabilities
If you discover a security vulnerability in the Postiz app, please report it through the [GitHub Security Advisory system](https://github.com/gitroomhq/postiz-app/security/advisories/new).
When reporting a security vulnerability, please provide as much detail as possible, including:
- A clear description of the vulnerability
- Proof of concept (PoC), where possible
- Steps to reproduce the vulnerability
- Any relevant code or configuration files
If the report has immediate urgency, please contact one (or more) of the maintainers via email:
- @egelhaus ([E-Mail](mailto:egelhaus@ennogelhaus.de))
### AI Reports
Reports that appear to be LLM-generated without meaningful human analysis — typically lacking a working proof of concept, reproducible steps, or accurate impact assessment — will be closed without detailed response.
Reports that include AI-assisted analysis are welcome provided they have been validated by the reporter and include a proof of concept, reproduction steps, and impact assessment.
## Disclosure Guidelines
We follow a private disclosure policy. If you discover a security vulnerability, please report it to us privately via email to one of the maintainers listed above. We will respond promptly to reports of vulnerabilities and work to resolve them as quickly as possible.
We follow a private disclosure policy. If you discover a security vulnerability, please report it to us privately via GitHub Security Advisories, and if immediate urgency, via email as listed above. We will respond promptly to reports of vulnerabilities and work to resolve them as quickly as possible.
We will not publicly disclose security vulnerabilities until a patch or fix is available to prevent malicious actors from exploiting the vulnerability before a fix is released.
@ -36,8 +58,12 @@ We take security vulnerabilities seriously and will respond promptly to reports
- Releasing the patch or fix as soon as possible.
- Notifying users of the vulnerability and the patch or fix.
## Template Attribution
## Response Timelines
This SECURITY.md file is based on the [GitHub Security Policy Template](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository).
We aim to follow these timelines:
Thank you for helping to keep the `postiz-app` secure!
- **Initial Acknowledgment:** Within 72 hours of initial report.
- **Completed Triage / Verification:** Within 7 days of initial acknowledgment.
- **Critical Issue Remediation:** Within 90 days of completed triage.
- **Non-Critical Issue Remediation:** Within 180 days of completed triage.
- **CVE Publication:** Within 24 hours of remediation release.

View file

@ -36,6 +36,8 @@ import { EnterpriseController } from '@gitroom/backend/api/routes/enterprise.con
import { OAuthAppController } from '@gitroom/backend/api/routes/oauth-app.controller';
import { ApprovedAppsController } from '@gitroom/backend/api/routes/approved-apps.controller';
import { OAuthController, OAuthAuthorizedController } from '@gitroom/backend/api/routes/oauth.controller';
import { AnnouncementsController } from '@gitroom/backend/api/routes/announcements.controller';
import { AdminController } from '@gitroom/backend/api/routes/admin.controller';
import { AuthProviderManager } from '@gitroom/backend/services/auth/providers/providers.manager';
import { GithubProvider } from '@gitroom/backend/services/auth/providers/github.provider';
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
@ -61,6 +63,8 @@ const authenticatedController = [
OAuthAppController,
ApprovedAppsController,
OAuthAuthorizedController,
AnnouncementsController,
AdminController,
];
@Module({
imports: [UploadModule],

View file

@ -0,0 +1,47 @@
import {
Controller,
Get,
HttpException,
Query,
} from '@nestjs/common';
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
import { User } from '@prisma/client';
import { ApiTags } from '@nestjs/swagger';
import { ErrorsService } from '@gitroom/nestjs-libraries/database/prisma/errors/errors.service';
@ApiTags('Admin')
@Controller('/admin')
export class AdminController {
constructor(private _errorsService: ErrorsService) {}
private assertSuperAdmin(user: User) {
if (!user?.isSuperAdmin) {
throw new HttpException('Unauthorized', 400);
}
}
@Get('/errors')
async listErrors(
@GetUserFromRequest() user: User,
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('platform') platform?: string,
@Query('email') email?: string,
@Query('unknownFirst') unknownFirst?: string
) {
this.assertSuperAdmin(user);
return this._errorsService.listErrors({
page: page ? parseInt(page, 10) : 0,
limit: limit ? parseInt(limit, 10) : 20,
platform: platform || undefined,
email: email || undefined,
unknownFirst: unknownFirst === 'true' || unknownFirst === '1',
});
}
@Get('/errors/platforms')
async listPlatforms(@GetUserFromRequest() user: User) {
this.assertSuperAdmin(user);
return this._errorsService.listPlatforms();
}
}

View file

@ -0,0 +1,47 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
} from '@nestjs/common';
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
import { User } from '@prisma/client';
import { ApiTags } from '@nestjs/swagger';
import { AnnouncementsService } from '@gitroom/nestjs-libraries/database/prisma/announcements/announcements.service';
import { AnnouncementDto } from '@gitroom/nestjs-libraries/dtos/announcements/announcements.dto';
@ApiTags('Announcements')
@Controller('/announcements')
export class AnnouncementsController {
constructor(private _announcementsService: AnnouncementsService) {}
@Get('/')
async getAnnouncements() {
return this._announcementsService.getAnnouncements();
}
@Post('/')
async createAnnouncement(
@GetUserFromRequest() user: User,
@Body() body: AnnouncementDto
) {
if (!user.isSuperAdmin) {
throw new HttpException('Unauthorized', 400);
}
return this._announcementsService.createAnnouncement(body);
}
@Delete('/:id')
async deleteAnnouncement(
@GetUserFromRequest() user: User,
@Param('id') id: string
) {
if (!user.isSuperAdmin) {
throw new HttpException('Unauthorized', 400);
}
return this._announcementsService.deleteAnnouncement(id);
}
}

View file

@ -199,6 +199,19 @@ export class AuthController {
};
}
@Get('/oauth-mobile-callback')
mobileCallback(
@Query('code') code: string,
@Query('state') state: string,
@Res({ passthrough: false }) response: Response
) {
const scheme = process.env.MOBILE_APP_SCHEME || 'postiz://auth/callback';
const params = new URLSearchParams();
if (code) params.set('code', code);
if (state) params.set('state', state);
return response.redirect(302, `${scheme}?${params.toString()}`);
}
@Get('/oauth/:provider')
async oauthLink(@Param('provider') provider: string, @Query() query: any) {
return this._authService.oauthLink(provider, query);
@ -210,7 +223,10 @@ export class AuthController {
@Body('datafast_visitor_id') datafast_visitor_id: string,
@Res({ passthrough: false }) response: Response
) {
const activate = await this._authService.activate(code, datafast_visitor_id);
const activate = await this._authService.activate(
code,
datafast_visitor_id
);
if (!activate) {
return response.status(200).json({ can: false });
}
@ -254,10 +270,15 @@ export class AuthController {
@Post('/oauth/:provider/exists')
async oauthExists(
@Body('code') code: string,
@Body('redirect_uri') redirect_uri: string,
@Param('provider') provider: string,
@Res({ passthrough: false }) response: Response
) {
const { jwt, token } = await this._authService.checkExists(provider, code);
const { jwt, token } = await this._authService.checkExists(
provider,
code,
redirect_uri
);
if (token) {
return response.json({ token });

View file

@ -15,6 +15,7 @@ import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permis
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
import { OnlyURL } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
@ApiTags('Autopost')
@Controller('/autopost')
@ -62,7 +63,7 @@ export class AutopostController {
}
@Post('/send')
async sendWebhook(@Query('url') url: string) {
return this._autopostsService.loadXML(url);
async sendWebhook(@Query() query: OnlyURL) {
return this._autopostsService.loadXML(query.url);
}
}

View file

@ -20,7 +20,7 @@ import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/s
import { MastraAgent } from '@ag-ui/mastra';
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
import { Request, Response } from 'express';
import { RuntimeContext } from '@mastra/core/di';
import { RequestContext } from '@mastra/core/di';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
@ -72,20 +72,19 @@ export class CopilotController {
return;
}
const mastra = await this._mastraService.mastra();
const runtimeContext = new RuntimeContext<ChannelsContext>();
runtimeContext.set(
const requestContext = new RequestContext<ChannelsContext>();
requestContext.set(
'integrations',
req?.body?.variables?.properties?.integrations || []
);
runtimeContext.set('organization', JSON.stringify(organization));
runtimeContext.set('ui', 'true');
requestContext.set('organization', JSON.stringify(organization));
requestContext.set('ui', 'true');
const agents = MastraAgent.getLocalAgents({
resourceId: organization.id,
mastra,
// @ts-ignore
runtimeContext,
requestContext: requestContext as any,
});
const runtime = new CopilotRuntime({
@ -124,7 +123,7 @@ export class CopilotController {
const mastra = await this._mastraService.mastra();
const memory = await mastra.getAgent('postiz').getMemory();
try {
return await memory.query({
return await memory.recall({
resourceId: organization.id,
threadId,
});
@ -137,14 +136,12 @@ export class CopilotController {
@CheckPolicies([AuthorizationActions.Create, Sections.AI])
async getList(@GetOrgFromRequest() organization: Organization) {
const mastra = await this._mastraService.mastra();
// @ts-ignore
const memory = await mastra.getAgent('postiz').getMemory();
const list = await memory.getThreadsByResourceIdPaginated({
resourceId: organization.id,
const list = await memory.listThreads({
filter: { resourceId: organization.id },
perPage: 100000,
page: 0,
orderBy: 'createdAt',
sortDirection: 'DESC',
orderBy: { field: 'createdAt', direction: 'DESC' },
});
return {

View file

@ -101,6 +101,7 @@ export class IntegrationsController {
internalId: p.internalId,
disabled: p.disabled,
editor: findIntegration.editor,
stripLinks: !!findIntegration?.stripLinks?.(),
picture: p.picture || '/no-picture.jpg',
identifier: p.providerIdentifier,
inBetweenSteps: p.inBetweenSteps,
@ -195,6 +196,7 @@ export class IntegrationsController {
@Param('integration') integration: string,
@Query('refresh') refresh: string,
@Query('externalUrl') externalUrl: string,
@Query('redirectUrl') redirectUrl: string,
@Query('onboarding') onboarding: string,
@GetOrgFromRequest() org: Organization
) {
@ -232,6 +234,10 @@ export class IntegrationsController {
await ioRedis.set(`onboarding:${state}`, 'true', 'EX', 3600);
}
if (redirectUrl) {
await ioRedis.set(`redirect:${state}`, redirectUrl, 'EX', 3600);
}
await ioRedis.set(`organization:${state}`, org.id, 'EX', 3600);
await ioRedis.set(`login:${state}`, codeVerifier, 'EX', 3600);
await ioRedis.set(
@ -448,9 +454,7 @@ export class IntegrationsController {
}
@Post('/moltbook/register')
async moltbookRegister(
@Body() body: { name: string; description: string }
) {
async moltbookRegister(@Body() body: { name: string; description: string }) {
try {
const provider = new MoltbookProvider();
const result = await provider.registerAgent(body.name, body.description);

View file

@ -129,6 +129,7 @@ export class MediaController {
@Post('/upload-simple')
@UseInterceptors(FileInterceptor('file'))
@UsePipes(new CustomFileValidationPipe())
async uploadSimple(
@GetOrgFromRequest() org: Organization,
@UploadedFile('file') file: Express.Multer.File,
@ -180,9 +181,10 @@ export class MediaController {
@Get('/')
getMedia(
@GetOrgFromRequest() org: Organization,
@Query('page') page: number
@Query('page') page: number,
@Query('search') search?: string
) {
return this._mediaService.getMedia(org.id, page);
return this._mediaService.getMedia(org.id, page, search);
}
@Get('/video-options')

View file

@ -3,6 +3,7 @@ import {
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put,
@ -144,6 +145,18 @@ export class PostsController {
return this._postsService.getOldPosts(org.id, date);
}
@Get('/group/:group/debug-export')
async getPostGroupDebugExport(
@GetOrgFromRequest() org: Organization,
@GetUserFromRequest() user: User,
@Param('group') group: string
) {
if (!user.isSuperAdmin) {
throw new HttpException('Forbidden', 403);
}
return this._postsService.getPostGroupDebugExport(org.id, group);
}
@Get('/group/:group')
getPostsByGroup(@GetOrgFromRequest() org: Organization, @Param('group') group: string) {
return this._postsService.getPostsByGroup(org.id, group);
@ -162,7 +175,7 @@ export class PostsController {
) {
console.log(JSON.stringify(rawBody, null, 2));
const body = await this._postsService.mapTypeToPost(rawBody, org.id);
return this._postsService.createPost(org.id, body);
return this._postsService.createPost(org.id, body, 'WEB');
}
@Post('/generator/draft')

View file

@ -10,7 +10,6 @@ import {
StreamableFile,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
import { RealIP } from 'nestjs-real-ip';
@ -26,6 +25,9 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import { Readable, pipeline } from 'stream';
import { promisify } from 'util';
import { OnlyURL } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator';
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
const pump = promisify(pipeline);
@ -33,7 +35,6 @@ const pump = promisify(pipeline);
@Controller('/public')
export class PublicController {
constructor(
private _agenciesService: AgenciesService,
private _trackService: TrackService,
private _agentGraphInsertService: AgentGraphInsertService,
private _postsService: PostsService,
@ -52,26 +53,6 @@ export class PublicController {
return this._agentGraphInsertService.newPost(body.text);
}
@Get('/agencies-list')
async getAgencyByUser() {
return this._agenciesService.getAllAgencies();
}
@Get('/agencies-list-slug')
async getAgencySlug() {
return this._agenciesService.getAllAgenciesSlug();
}
@Get('/agencies-information/:agency')
async getAgencyInformation(@Param('agency') agency: string) {
return this._agenciesService.getAgencyInformation(agency);
}
@Get('/agencies-list-count')
async getAgenciesCount() {
return this._agenciesService.getCount();
}
@Get(`/posts/:id`)
async getPreview(@Param('id') id: string) {
return (await this._postsService.getPostsRecursively(id, true)).map(
@ -183,10 +164,11 @@ export class PublicController {
@Get('/stream')
async streamFile(
@Query('url') url: string,
@Query() query: OnlyURL,
@Res() res: Response,
@Req() req: Request
) {
const { url } = query;
if (!url.endsWith('mp4')) {
return res.status(400).send('Invalid video URL');
}
@ -196,7 +178,47 @@ export class PublicController {
req.on('aborted', onClose);
res.on('close', onClose);
const r = await fetch(url, { signal: ac.signal });
// Manually follow redirects so every hop is re-validated against
// the SSRF blocklist (see GHSA-34w8-5j2v-h6ww). `fetch` defaults to
// `redirect: 'follow'`, which bypasses the DTO-level URL check.
const MAX_REDIRECTS = 5;
let currentUrl = url;
let r: globalThis.Response | undefined;
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
if (!(await isSafePublicHttpsUrl(currentUrl))) {
return res.status(400).send('Blocked URL');
}
r = await fetch(currentUrl, {
signal: ac.signal,
redirect: 'manual',
// @ts-ignore — undici option, not in lib.dom fetch types
dispatcher: ssrfSafeDispatcher,
});
if (r.status >= 300 && r.status < 400) {
const location = r.headers.get('location');
if (!location) {
return res.status(502).send('Redirect without Location');
}
try {
currentUrl = new URL(location, currentUrl).toString();
} catch {
return res.status(400).send('Invalid redirect target');
}
continue;
}
break;
}
if (!r) {
return res.status(502).send('No upstream response');
}
if (r.status >= 300 && r.status < 400) {
return res.status(508).send('Too many redirects');
}
if (!r.ok && r.status !== 206) {
res.status(r.status);
@ -219,7 +241,6 @@ export class PublicController {
try {
await pump(Readable.fromWeb(r.body as any), res);
} catch (err) {
}
} catch (err) {}
}
}

View file

@ -14,6 +14,7 @@ import { Organization } from '@prisma/client';
import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
import { ImportMediaDto } from '@gitroom/nestjs-libraries/dtos/third-party/import-media.dto';
@ApiTags('Third Party')
@Controller('/third-party')
@ -121,6 +122,52 @@ export class ThirdPartyController {
);
}
@Post('/:id/import')
async importMedia(
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string,
@Body() body: ImportMediaDto
) {
const thirdParty = await this._thirdPartyManager.getIntegrationById(
organization.id,
id
);
if (!thirdParty) {
throw new HttpException('Integration not found', 404);
}
const thirdPartyInstance = this._thirdPartyManager.getThirdPartyByName(
thirdParty.identifier
);
if (!thirdPartyInstance) {
throw new HttpException('Invalid identifier', 400);
}
const downloadUrls = await thirdPartyInstance?.instance?.['importMedia']?.(
AuthService.fixedDecryption(thirdParty.apiKey),
body.items
);
if (!downloadUrls || !Array.isArray(downloadUrls)) {
throw new HttpException('Import not supported', 400);
}
const results = [];
for (const item of downloadUrls) {
const file = await this.storage.uploadSimple(item.url);
const saved = await this._mediaService.saveFile(
organization.id,
item.name || file.split('/').pop(),
file
);
results.push(saved);
}
return results;
}
@Post('/:identifier')
async addApiKey(
@GetOrgFromRequest() organization: Organization,

View file

@ -14,8 +14,7 @@ import { ApiTags } from '@nestjs/swagger';
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import {
UpdateDto,
WebhooksDto,
OnlyURL, UpdateDto, WebhooksDto
} from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
@ -55,9 +54,9 @@ export class WebhookController {
}
@Post('/send')
async sendWebhook(@Body() body: any, @Query('url') url: string) {
async sendWebhook(@Body() body: any, @Query() query: OnlyURL) {
try {
await fetch(url, {
await fetch(query.url, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },

View file

@ -36,7 +36,7 @@ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
throttlers: [
{
ttl: 3600000,
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 30,
limit: process.env.API_LIMIT ? Number(process.env.API_LIMIT) : 90,
},
],
storage: new ThrottlerStorageRedisService(ioRedis),

View file

@ -27,6 +27,9 @@ async function start() {
allowedHeaders: [
'Content-Type',
'Authorization',
'auth',
'showorg',
'impersonate',
'x-copilotkit-runtime-client-gql-version',
],
exposedHeaders: [
@ -52,7 +55,7 @@ async function start() {
})
);
app.use(['/copilot/*', '/posts'], (req: any, res: any, next: any) => {
app.use(['/copilot/{*splat}', '/posts'], (req: any, res: any, next: any) => {
json({ limit: '50mb' })(req, res, next);
});
@ -67,6 +70,7 @@ async function start() {
try {
await app.listen(port);
console.log('Backend started successfully on port ' + port);
checkConfiguration(); // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up.

View file

@ -10,7 +10,9 @@ import {
Query,
UploadedFile,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
import { ApiTags } from '@nestjs/swagger';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { Organization } from '@prisma/client';
@ -21,6 +23,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import { ChangePostStatusDto } from '@gitroom/nestjs-libraries/dtos/posts/change.post.status.dto';
import {
AuthorizationActions,
Sections,
@ -30,11 +33,26 @@ import { VideoFunctionDto } from '@gitroom/nestjs-libraries/dtos/videos/video.fu
import { UploadDto } from '@gitroom/nestjs-libraries/dtos/media/upload.dto';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
import { GetNotificationsDto } from '@gitroom/nestjs-libraries/dtos/notifications/get.notifications.dto';
import axios from 'axios';
import { Readable } from 'stream';
import { lookup, extension } from 'mime-types';
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { fromBuffer } = require('file-type');
const PUBLIC_API_ALLOWED_MIME = new Set<string>([
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/avif',
'image/bmp',
'image/tiff',
'video/mp4',
]);
import * as Sentry from '@sentry/nestjs';
import { socialIntegrationList, IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import {
socialIntegrationList,
IntegrationManager,
} from '@gitroom/nestjs-libraries/integrations/integration.manager';
import { getValidationSchemas } from '@gitroom/nestjs-libraries/chat/validation.schemas.helper';
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
@ -57,6 +75,7 @@ export class PublicIntegrationsController {
@Post('/upload')
@UseInterceptors(FileInterceptor('file'))
@UsePipes(new CustomFileValidationPipe())
async uploadSimple(
@GetOrgFromRequest() org: Organization,
@UploadedFile('file') file: Express.Multer.File
@ -80,15 +99,20 @@ export class PublicIntegrationsController {
@Body() body: UploadDto
) {
Sentry.metrics.count('public_api-request', 1);
const response = await axios.get(body.url, {
responseType: 'arraybuffer',
const response = await fetch(body.url, {
// @ts-ignore — undici option, not in lib.dom fetch types
dispatcher: ssrfSafeDispatcher,
});
const buffer = Buffer.from(response.data);
const responseMime = response.headers?.['content-type']?.split(';')[0]?.trim();
const urlMime = lookup(body?.url?.split?.('?')?.[0]);
const mimetype = (urlMime || responseMime || 'image/jpeg') as string;
const ext = extension(mimetype) || 'jpg';
if (!response.ok) {
throw new HttpException({ msg: 'Failed to fetch URL' }, 400);
}
const buffer = Buffer.from(await response.arrayBuffer());
const detected = await fromBuffer(buffer);
if (!detected || !PUBLIC_API_ALLOWED_MIME.has(detected.mime)) {
throw new HttpException({ msg: 'Unsupported file type.' }, 400);
}
const mimetype = detected.mime;
const ext = detected.ext;
const getFile = await this.storage.uploadFile({
buffer,
@ -146,8 +170,33 @@ export class PublicIntegrationsController {
);
body.type = rawBody.type;
if (
process.env.RESTRICT_UPLOAD_DOMAINS &&
body.posts.some((p) =>
p.value.some((a) =>
a.image.some(
(i) => i.path.indexOf(process.env.RESTRICT_UPLOAD_DOMAINS) === -1
)
)
)
) {
throw new HttpException(
{
msg: `All media must be uploaded through our upload API route and contain the domain: ${process.env.RESTRICT_UPLOAD_DOMAINS}`,
},
400
);
}
const allowedCreationMethods = ['CLI', 'API'] as const;
const creationMethod = allowedCreationMethods.includes(
rawBody.creationMethod
)
? (rawBody.creationMethod as 'CLI' | 'API')
: 'API';
console.log(JSON.stringify(body, null, 2));
return this._postsService.createPost(org.id, body);
return this._postsService.createPost(org.id, body, creationMethod);
}
@Delete('/posts/:id')
@ -217,7 +266,9 @@ export class PublicIntegrationsController {
if (integrationProvider.externalUrl) {
throw new HttpException(
{ msg: 'This integration requires an external URL and is not supported via the public API' },
{
msg: 'This integration requires an external URL and is not supported via the public API',
},
400
);
}
@ -341,6 +392,16 @@ export class PublicIntegrationsController {
return this._postsService.getMissingContent(org.id, id);
}
@Put('/posts/:id/status')
async changePostStatus(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string,
@Body() body: ChangePostStatusDto
) {
Sentry.metrics.count('public_api-request', 1);
return this._postsService.changePostStatus(org.id, id, body.status);
}
@Put('/posts/:id/release-id')
async updateReleaseId(
@GetOrgFromRequest() org: Organization,

View file

@ -43,6 +43,9 @@ export class AuthService {
if (process.env.DISALLOW_PLUS && body.email.includes('+')) {
throw new Error('Email with plus sign is not allowed');
}
if (body instanceof CreateOrgUserDto) {
body.email = body.email.toLowerCase();
}
const user = await this._userService.getUserByEmail(body.email);
if (body instanceof CreateOrgUserDto) {
if (user) {
@ -290,9 +293,9 @@ export class AuthService {
return providerInstance.generateLink(query);
}
async checkExists(provider: string, code: string) {
async checkExists(provider: string, code: string, redirectUri?: string) {
const providerInstance = this._providerManager.getProvider(provider);
const token = await providerInstance.getToken(code);
const token = await providerInstance.getToken(code, redirectUri);
const user = await providerInstance.getUser(token);
if (!user) {
throw new Error('Invalid user');
@ -309,6 +312,9 @@ export class AuthService {
}
private async jwt(user: User) {
if (user.password) {
delete user.password;
}
return AuthChecker.signJWT(user);
}
}

View file

@ -44,8 +44,10 @@ export class PoliciesGuard implements CanActivate {
// @ts-expect-error
const { org }: { org: Organization } = request;
const refreshChannelId = typeof request.query?.refresh === 'string' ? request.query.refresh : undefined;
// @ts-ignore
const ability = await this._authorizationService.check(org.id, org.createdAt, org.users[0].role, policyHandlers);
const ability = await this._authorizationService.check(org.id, org.createdAt, org.users[0].role, policyHandlers, refreshChannelId);
const item = policyHandlers.find(
(handler) => !this.execPolicyHandler(handler, ability)

View file

@ -40,7 +40,8 @@ export class PermissionsService {
orgId: string,
created_at: Date,
permission: 'USER' | 'ADMIN' | 'SUPERADMIN',
requestedPermission: Array<[AuthorizationActions, Sections]>
requestedPermission: Array<[AuthorizationActions, Sections]>,
refreshChannelId?: string
) {
const { can, build } = new AbilityBuilder<
Ability<[AuthorizationActions, Sections]>
@ -65,6 +66,20 @@ export class PermissionsService {
for (const [action, section] of requestedPermission) {
// check for the amount of channels
if (section === Sections.CHANNEL) {
// Refreshing an existing channel doesn't add a new one, so skip the limit check
// but only if the channel actually belongs to this org
if (refreshChannelId) {
const existingIntegration =
await this._integrationService.getIntegrationById(
orgId,
refreshChannelId
);
if (existingIntegration) {
can(action, section);
continue;
}
}
const totalChannels = (
await this._integrationService.getIntegrationsList(orgId)
).filter((f) => !f.refreshNeeded).length;

View file

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
export abstract class AuthProviderAbstract {
abstract generateLink(query?: any): Promise<string> | string;
abstract getToken(code: string): Promise<string>;
abstract getToken(code: string, redirectUri?: string): Promise<string>;
abstract getUser(
providerToken: string
): Promise<{ email: string; id: string }> | false;

View file

@ -14,7 +14,7 @@ export class FarcasterProvider extends AuthProviderAbstract {
return '';
}
async getToken(code: string) {
async getToken(code: string, _redirectUri?: string) {
const data = JSON.parse(Buffer.from(code, 'base64').toString());
const status = await client.lookupSigner({ signerUuid: data.signer_uuid });
if (status.status === 'approved') {

View file

@ -13,7 +13,7 @@ export class GithubProvider extends AuthProviderAbstract {
)}`;
}
async getToken(code: string): Promise<string> {
async getToken(code: string, _redirectUri?: string): Promise<string> {
const { access_token } = await (
await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',

View file

@ -1,48 +1,28 @@
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library/build/src/auth/oauth2client';
import {
AuthProvider,
AuthProviderAbstract,
} from '@gitroom/backend/services/auth/providers.interface';
const clientAndYoutube = () => {
const client = new google.auth.OAuth2({
const defaultRedirect = () =>
`${process.env.FRONTEND_URL}/integrations/social/youtube`;
const makeClient = (redirectUri: string) =>
new google.auth.OAuth2({
clientId: process.env.YOUTUBE_CLIENT_ID,
clientSecret: process.env.YOUTUBE_CLIENT_SECRET,
redirectUri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
redirectUri,
});
const youtube = (newClient: OAuth2Client) =>
google.youtube({
version: 'v3',
auth: newClient,
});
const youtubeAnalytics = (newClient: OAuth2Client) =>
google.youtubeAnalytics({
version: 'v2',
auth: newClient,
});
const oauth2 = (newClient: OAuth2Client) =>
google.oauth2({
version: 'v2',
auth: newClient,
});
return { client, youtube, oauth2, youtubeAnalytics };
};
@AuthProvider({ provider: 'GOOGLE' })
export class GoogleProvider extends AuthProviderAbstract {
generateLink() {
const state = 'login';
const { client } = clientAndYoutube();
return client.generateAuthUrl({
generateLink(query?: { redirect_uri?: string }) {
const redirectUri = query?.redirect_uri || defaultRedirect();
return makeClient(redirectUri).generateAuthUrl({
access_type: 'online',
prompt: 'consent',
state,
redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/youtube`,
state: 'login',
redirect_uri: redirectUri,
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
@ -50,21 +30,22 @@ export class GoogleProvider extends AuthProviderAbstract {
});
}
async getToken(code: string) {
const { client, oauth2 } = clientAndYoutube();
async getToken(code: string, redirectUri?: string) {
const client = makeClient(redirectUri || defaultRedirect());
const { tokens } = await client.getToken(code);
return tokens.access_token;
return tokens.access_token!;
}
async getUser(providerToken: string) {
const { client, oauth2 } = clientAndYoutube();
const client = makeClient(defaultRedirect());
client.setCredentials({ access_token: providerToken });
const user = oauth2(client);
const { data } = await user.userinfo.get();
const { data } = await google
.oauth2({ version: 'v2', auth: client })
.userinfo.get();
return {
id: data.id!,
email: data.email,
email: data.email!,
};
}
}

View file

@ -48,7 +48,7 @@ export class OauthProvider extends AuthProviderAbstract {
return `${authUrl}?${params.toString()}`;
}
async getToken(code: string): Promise<string> {
async getToken(code: string, _redirectUri?: string): Promise<string> {
const { tokenUrl, clientId, clientSecret, frontendUrl } = this.getConfig();
const response = await fetch(`${tokenUrl}`, {
method: 'POST',

View file

@ -40,7 +40,7 @@ export class WalletProvider extends AuthProviderAbstract {
return challenge;
}
async getToken(code: string) {
async getToken(code: string, _redirectUri?: string) {
const { publicKey, challenge, signature } = JSON.parse(
Buffer.from(code, 'base64').toString()
);

4
apps/cli/.gitignore vendored
View file

@ -1,4 +0,0 @@
node_modules
dist
*.log
.DS_Store

View file

@ -1,9 +0,0 @@
src
examples
tsconfig.json
tsup.config.ts
*.md
!README.md
node_modules
.git
.gitignore

View file

@ -1,29 +0,0 @@
# Changelog
All notable changes to the Postiz CLI will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2026-02-13
### Added
- Initial release of Postiz CLI
- `posts:create` - Create new social media posts
- `posts:list` - List all posts with pagination and search
- `posts:delete` - Delete posts by ID
- `integrations:list` - List connected social media integrations
- `upload` - Upload media files (images)
- Environment variable configuration (POSTIZ_API_KEY, POSTIZ_API_URL)
- Comprehensive help documentation
- Example scripts for basic usage and AI agent integration
- SKILL.md for AI agent usage patterns
### Features
- Command-line interface for Postiz API
- Support for scheduled posts
- Multi-platform posting via integrations
- Media upload functionality
- User-friendly error messages with emojis
- JSON output for programmatic parsing
- Comprehensive examples for AI agents

View file

@ -1,287 +0,0 @@
# Postiz CLI - Feature Summary
## ✅ Complete Feature Set
### Posts with Comments and Media - FULLY SUPPORTED
The Postiz CLI **fully supports** the complete API structure including:
#### ✅ Posts with Comments
- Main post content
- Multiple comments/replies
- Each comment can have different content
- Configurable delays between comments
#### ✅ Multiple Media per Post/Comment
- Each post can have **multiple images** (array of MediaDto)
- Each comment can have **its own images** (separate MediaDto arrays)
- Support for various image formats (PNG, JPG, JPEG, GIF)
- Media can be URLs or uploaded files
#### ✅ Multi-Platform Posting
- Post to multiple platforms in one request
- Platform-specific content for each integration
- Different media for different platforms
#### ✅ Advanced Features
- Scheduled posting with precise timestamps
- URL shortening support
- Tags and metadata
- Delays between comments (in milliseconds)
- Draft mode for review before posting
## Usage Modes
### 1. Simple Mode (Command Line)
For quick, simple posts:
```bash
# Single post
postiz posts:create -c "Hello!" -i "twitter-123"
# With multiple images
postiz posts:create -c "Post" --image "img1.jpg,img2.jpg,img3.jpg" -i "twitter-123"
# With comments (no custom media per comment)
postiz posts:create -c "Main" --comments "Comment 1;Comment 2" -i "twitter-123"
```
**Limitations of Simple Mode:**
- Comments share the same media as the main post
- Cannot specify different images for each comment
- Cannot set custom delays between comments
### 2. Advanced Mode (JSON Files)
For complex posts with comments that have their own media:
```bash
postiz posts:create --json complex-post.json
```
**Capabilities:**
- ✅ Each comment can have different media
- ✅ Custom delays between comments
- ✅ Multiple posts to different platforms
- ✅ Platform-specific content and media
- ✅ Full control over all API features
## Real-World Examples
### Example 1: Product Launch with Follow-up Comments
**Main Post:** Product announcement with 3 product images
**Comment 1:** Feature highlight with 1 feature screenshot (posted 1 hour later)
**Comment 2:** Special offer with 1 promotional image (posted 2 hours later)
```json
{
"type": "schedule",
"date": "2024-03-15T09:00:00Z",
"posts": [{
"integration": { "id": "twitter-123" },
"value": [
{
"content": "🚀 Launching our new product!",
"image": [
{ "id": "p1", "path": "product-1.jpg" },
{ "id": "p2", "path": "product-2.jpg" },
{ "id": "p3", "path": "product-3.jpg" }
]
},
{
"content": "⭐ Key features you'll love:",
"image": [
{ "id": "f1", "path": "features-screenshot.jpg" }
],
"delay": 3600000
},
{
"content": "🎁 Limited time: 50% off!",
"image": [
{ "id": "o1", "path": "special-offer.jpg" }
],
"delay": 7200000
}
]
}]
}
```
### Example 2: Tutorial Thread
**Main Post:** Introduction with overview image
**Tweets 2-5:** Step-by-step with different screenshots for each step
```json
{
"type": "now",
"posts": [{
"integration": { "id": "twitter-123" },
"value": [
{
"content": "🧵 How to use our CLI (1/5)",
"image": [{ "id": "1", "path": "overview.jpg" }]
},
{
"content": "Step 1: Installation (2/5)",
"image": [{ "id": "2", "path": "step1.jpg" }],
"delay": 2000
},
{
"content": "Step 2: Configuration (3/5)",
"image": [{ "id": "3", "path": "step2.jpg" }],
"delay": 2000
},
{
"content": "Step 3: First post (4/5)",
"image": [{ "id": "4", "path": "step3.jpg" }],
"delay": 2000
},
{
"content": "You're all set! 🎉 (5/5)",
"image": [{ "id": "5", "path": "done.jpg" }],
"delay": 2000
}
]
}]
}
```
### Example 3: Multi-Platform Campaign
**Same event, different content per platform:**
```json
{
"type": "schedule",
"date": "2024-12-25T12:00:00Z",
"posts": [
{
"integration": { "id": "twitter-123" },
"value": [
{
"content": "Short, catchy Twitter post 🐦",
"image": [{ "id": "t1", "path": "twitter-square.jpg" }]
},
{
"content": "Thread continuation with details",
"image": [{ "id": "t2", "path": "twitter-details.jpg" }],
"delay": 5000
}
]
},
{
"integration": { "id": "linkedin-456" },
"value": [{
"content": "Professional, detailed LinkedIn post with business context...",
"image": [
{ "id": "l1", "path": "linkedin-wide.jpg" },
{ "id": "l2", "path": "linkedin-graph.jpg" }
]
}]
},
{
"integration": { "id": "facebook-789" },
"value": [
{
"content": "Engaging Facebook post for family/friends audience",
"image": [
{ "id": "f1", "path": "facebook-photo1.jpg" },
{ "id": "f2", "path": "facebook-photo2.jpg" },
{ "id": "f3", "path": "facebook-photo3.jpg" }
]
},
{
"content": "More info in the comments!",
"image": [{ "id": "f4", "path": "facebook-cta.jpg" }],
"delay": 300000
}
]
}
]
}
```
## API Structure Reference
### Complete CreatePostDto
```typescript
{
type: 'now' | 'schedule' | 'draft' | 'update',
date: string, // ISO 8601 date
shortLink: boolean,
tags: Array<{
value: string,
label: string
}>,
posts: Array<{
integration: {
id: string // From integrations:list
},
value: Array<{ // Main post + comments
content: string,
image: Array<{ // Multiple images per post/comment
id: string,
path: string,
alt?: string,
thumbnail?: string
}>,
delay?: number, // Milliseconds
id?: string
}>,
settings: {
__type: 'EmptySettings'
}
}>
}
```
## For AI Agents
### When to Use Simple Mode
- Quick single posts
- No need for comment-specific media
- Posting to 1-2 platforms
- Same content across platforms
### When to Use Advanced Mode (JSON)
- ✅ **Comments need their own media** ← YOUR USE CASE
- ✅ Multi-platform with different content
- ✅ Threads with step-by-step images
- ✅ Timed follow-up comments
- ✅ Complex campaigns
### AI Agent Tips
1. **Generate JSON programmatically** - Don't write JSON manually
2. **Validate structure** - Use TypeScript types or JSON schema
3. **Test with "draft" type** - Review before posting
4. **Use unique image IDs** - Generate with UUID or random strings
5. **Set appropriate delays** - Twitter: 2-5s, others: 30s-1min+
## Files and Documentation
- **examples/post-with-comments.json** - Post with comments, each having media
- **examples/multi-platform-post.json** - Multi-platform campaign
- **examples/thread-post.json** - Twitter thread example
- **examples/EXAMPLES.md** - Comprehensive guide with all patterns
- **SKILL.md** - Full AI agent usage guide
- **README.md** - Installation and basic usage
## Summary
### Question: Does it support posts with comments, each with media?
**Answer: YES! ✅**
- ✅ Posts can have multiple comments
- ✅ Each comment can have its own media (multiple images)
- ✅ Each post can have multiple images
- ✅ Use JSON files for full control
- ✅ See examples/ directory for working templates
- ✅ Fully compatible with the Postiz API structure
The CLI supports the **complete Postiz API** including all advanced features!

View file

@ -1,300 +0,0 @@
# How to Run the Postiz CLI
There are several ways to run the CLI, depending on your needs.
## Option 1: Direct Execution (Quick Test) ⚡
The built file at `apps/cli/dist/index.js` is already executable!
```bash
# From the monorepo root
node apps/cli/dist/index.js --help
# Or run it directly (it has a shebang)
./apps/cli/dist/index.js --help
# Example command
export POSTIZ_API_KEY=your_key
node apps/cli/dist/index.js posts:list
```
## Option 2: Link Globally (Recommended for Development) 🔗
This creates a global `postiz` command you can use anywhere:
```bash
# From the monorepo root
cd apps/cli
pnpm link --global
# Now you can use it anywhere!
postiz --help
postiz posts:list
postiz posts:create -c "Hello!" -i "twitter-123"
# To unlink later
pnpm unlink --global
```
**After linking, you can use `postiz` from any directory!**
## Option 3: Use pnpm Filter (From Root) 📦
```bash
# From the monorepo root
pnpm --filter postiz start -- --help
pnpm --filter postiz start -- posts:list
pnpm --filter postiz start -- posts:create -c "Hello" -i "twitter-123"
```
## Option 4: Use npm/npx (After Publishing) 🌐
Once published to npm:
```bash
# Install globally
npm install -g postiz
# Or use with npx (no install)
npx postiz --help
npx postiz posts:list
```
## Quick Setup Guide
### Step 1: Build the CLI
```bash
# From monorepo root
pnpm run build:cli
```
### Step 2: Set Your API Key
```bash
export POSTIZ_API_KEY=your_api_key_here
# To make it permanent, add to your shell profile:
echo 'export POSTIZ_API_KEY=your_api_key' >> ~/.bashrc
# or ~/.zshrc if you use zsh
```
### Step 3: Choose Your Method
**For quick testing:**
```bash
node apps/cli/dist/index.js --help
```
**For regular use (recommended):**
```bash
cd apps/cli
pnpm link --global
postiz --help
```
## Troubleshooting
### "Command not found: postiz"
If you linked globally but still get this error:
```bash
# Check if it's linked
which postiz
# If not found, try linking again
cd apps/cli
pnpm link --global
# Or check your PATH
echo $PATH
```
### "POSTIZ_API_KEY is not set"
```bash
export POSTIZ_API_KEY=your_key
# Verify it's set
echo $POSTIZ_API_KEY
```
### Permission Denied
If you get permission errors:
```bash
# Make the file executable
chmod +x apps/cli/dist/index.js
# Then try again
./apps/cli/dist/index.js --help
```
### Rebuild After Changes
After making code changes, rebuild:
```bash
pnpm run build:cli
```
If you linked globally, the changes will be reflected immediately (no need to re-link).
## Testing the CLI
### Test Help Command
```bash
postiz --help
postiz posts:create --help
```
### Test with Sample Command (requires API key)
```bash
export POSTIZ_API_KEY=your_key
# List integrations
postiz integrations:list
# Create a test post
postiz posts:create \
-c "Test post from CLI" \
-i "your-integration-id"
```
## Development Workflow
### 1. Make Changes
Edit files in `apps/cli/src/`
### 2. Rebuild
```bash
pnpm run build:cli
```
### 3. Test
```bash
# If linked globally
postiz --help
# Or direct execution
node apps/cli/dist/index.js --help
```
### 4. Watch Mode (Auto-rebuild)
```bash
# From apps/cli directory
pnpm run dev
# In another terminal, test your changes
postiz --help
```
## Environment Variables
### Required
- `POSTIZ_API_KEY` - Your Postiz API key (required for all operations)
### Optional
- `POSTIZ_API_URL` - Custom API endpoint (default: `https://api.postiz.com`)
### Setting Environment Variables
**Temporary (current session):**
```bash
export POSTIZ_API_KEY=your_key
export POSTIZ_API_URL=https://custom-api.com
```
**Permanent (add to shell profile):**
```bash
# For bash
echo 'export POSTIZ_API_KEY=your_key' >> ~/.bashrc
source ~/.bashrc
# For zsh
echo 'export POSTIZ_API_KEY=your_key' >> ~/.zshrc
source ~/.zshrc
```
## Using Aliases
Create a convenient alias:
```bash
# Add to ~/.bashrc or ~/.zshrc
alias pz='postiz'
# Now you can use
pz posts:list
pz posts:create -c "Quick post" -i "twitter-123"
```
## Production Deployment
### Publish to npm
```bash
# From monorepo root
pnpm run publish-cli
# Or from apps/cli
cd apps/cli
pnpm run publish
```
### Install from npm
```bash
# Global install
npm install -g postiz
# Project-specific
npm install postiz
npx postiz --help
```
## Summary of Methods
| Method | Command | Use Case |
|--------|---------|----------|
| **Direct Node** | `node apps/cli/dist/index.js` | Quick testing, no installation |
| **Direct Execution** | `./apps/cli/dist/index.js` | Same as above, slightly shorter |
| **Global Link** | `postiz` (after `pnpm link --global`) | **Recommended** for development |
| **pnpm Filter** | `pnpm --filter postiz start --` | From monorepo root |
| **npm Global** | `postiz` (after `npm i -g postiz`) | After publishing to npm |
| **npx** | `npx postiz` | One-off usage without installing |
## Recommended Setup
For the best development experience:
```bash
# 1. Build
pnpm run build:cli
# 2. Link globally
cd apps/cli
pnpm link --global
# 3. Set API key
export POSTIZ_API_KEY=your_key
# 4. Test
postiz --help
postiz integrations:list
# 5. Start using!
postiz posts:create -c "My first post" -i "twitter-123"
```
Now you can use `postiz` from anywhere! 🚀

View file

@ -1,418 +0,0 @@
# Integration Settings Discovery
The CLI now has a powerful feature to discover what settings are available for each integration!
## New Command: `integrations:settings`
Get the settings schema, validation rules, and maximum character limits for any integration.
## Usage
```bash
postiz integrations:settings <integration-id>
```
## What It Returns
```json
{
"output": {
"maxLength": 280,
"settings": {
"properties": {
"who_can_reply_post": {
"enum": ["everyone", "following", "mentionedUsers", "subscribers", "verified"],
"description": "Who can reply to this post"
},
"community": {
"pattern": "^(https://x.com/i/communities/\\d+)?$",
"description": "X community URL"
}
},
"required": ["who_can_reply_post"]
}
}
}
```
## Workflow
### 1. List Your Integrations
```bash
postiz integrations:list
```
Output:
```json
[
{
"id": "reddit-abc123",
"name": "My Reddit Account",
"identifier": "reddit",
"provider": "reddit"
},
{
"id": "youtube-def456",
"name": "My YouTube Channel",
"identifier": "youtube",
"provider": "youtube"
},
{
"id": "twitter-ghi789",
"name": "@myhandle",
"identifier": "x",
"provider": "x"
}
]
```
### 2. Get Settings for Specific Integration
```bash
postiz integrations:settings reddit-abc123
```
Output:
```json
{
"output": {
"maxLength": 40000,
"settings": {
"properties": {
"subreddit": {
"type": "array",
"items": {
"properties": {
"value": {
"properties": {
"subreddit": {
"type": "string",
"minLength": 2,
"description": "Subreddit name"
},
"title": {
"type": "string",
"minLength": 2,
"description": "Post title"
},
"type": {
"type": "string",
"description": "Post type (text or link)"
},
"url": {
"type": "string",
"description": "URL for link posts"
},
"is_flair_required": {
"type": "boolean",
"description": "Whether flair is required"
},
"flair": {
"properties": {
"id": "string",
"name": "string"
}
}
},
"required": ["subreddit", "title", "type", "is_flair_required"]
}
}
}
}
},
"required": ["subreddit"]
}
}
}
```
### 3. Use the Settings in Your Post
Now you know what settings are available and required!
```bash
postiz posts:create \
-c "My post content" \
-p reddit \
--settings '{
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "Check this out!",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}' \
-i "reddit-abc123"
```
## Examples by Platform
### Reddit
```bash
postiz integrations:settings reddit-abc123
```
Returns:
- Max length: 40,000 characters
- Required settings: subreddit, title, type
- Optional: flair
### YouTube
```bash
postiz integrations:settings youtube-def456
```
Returns:
- Max length: 5,000 characters (description)
- Required settings: title, type (public/private/unlisted)
- Optional: tags, thumbnail, selfDeclaredMadeForKids
### X (Twitter)
```bash
postiz integrations:settings twitter-ghi789
```
Returns:
- Max length: 280 characters (or 4,000 for verified)
- Required settings: who_can_reply_post
- Optional: community
### LinkedIn
```bash
postiz integrations:settings linkedin-jkl012
```
Returns:
- Max length: 3,000 characters
- Optional settings: post_as_images_carousel, carousel_name
### TikTok
```bash
postiz integrations:settings tiktok-mno345
```
Returns:
- Max length: 150 characters (caption)
- Required settings: privacy_level, duet, stitch, comment, autoAddMusic, brand_content_toggle, brand_organic_toggle, content_posting_method
- Optional: title, video_made_with_ai
### Instagram
```bash
postiz integrations:settings instagram-pqr678
```
Returns:
- Max length: 2,200 characters
- Required settings: post_type (post or story)
- Optional: is_trial_reel, graduation_strategy, collaborators
## No Additional Settings Required
Some platforms don't require specific settings:
```bash
postiz integrations:settings threads-stu901
```
Returns:
```json
{
"output": {
"maxLength": 500,
"settings": "No additional settings required"
}
}
```
Platforms with no additional settings:
- Threads
- Mastodon
- Bluesky
- Telegram
- Nostr
- VK
## Use Cases
### 1. Discovery
Find out what settings are available before posting:
```bash
# What settings does YouTube support?
postiz integrations:settings youtube-123
# What settings does Reddit support?
postiz integrations:settings reddit-456
```
### 2. Validation
Check maximum character limits:
```bash
postiz integrations:settings twitter-789 | jq '.output.maxLength'
# Output: 280
```
### 3. AI Agent Integration
AI agents can call this endpoint to:
- Discover available settings dynamically
- Validate settings before posting
- Adapt to platform-specific requirements
```javascript
// Get settings schema
const settings = await execSync(
`postiz integrations:settings ${integrationId}`,
{ encoding: 'utf-8' }
);
const schema = JSON.parse(settings);
// Check max length
if (content.length > schema.output.maxLength) {
content = content.substring(0, schema.output.maxLength);
}
// Use required settings
const requiredSettings = schema.output.settings.required || [];
```
### 4. Form Generation
Use the schema to generate UI forms:
```javascript
const settings = await getIntegrationSettings('reddit-123');
const schema = settings.output.settings;
// Generate form fields from schema
schema.properties.subreddit.items.properties.value.properties
// → subreddit (text, minLength: 2)
// → title (text, minLength: 2)
// → type (select: text/link)
// → etc.
```
## Combined Workflow
Complete workflow for posting with correct settings:
```bash
#!/bin/bash
export POSTIZ_API_KEY=your_key
# 1. List integrations
echo "📋 Available integrations:"
postiz integrations:list
# 2. Get settings for Reddit
echo ""
echo "⚙️ Reddit settings:"
SETTINGS=$(postiz integrations:settings reddit-123)
echo $SETTINGS | jq '.output.maxLength'
echo $SETTINGS | jq '.output.settings'
# 3. Create post with correct settings
echo ""
echo "📝 Creating post..."
postiz posts:create \
-c "My post content" \
-p reddit \
--settings '{
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "Interesting post",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}' \
-i "reddit-123"
```
## API Endpoint
The command calls:
```
GET /public/v1/integration-settings/:id
```
Returns:
```typescript
{
output: {
maxLength: number;
settings: ValidationSchema | "No additional settings required";
}
}
```
## Error Handling
### Integration Not Found
```bash
postiz integrations:settings invalid-id
# ❌ Failed to get integration settings: Integration not found
```
### API Key Not Set
```bash
postiz integrations:settings reddit-123
# ❌ Error: POSTIZ_API_KEY environment variable is required
```
## Tips
1. **Always check settings first** before creating posts with custom settings
2. **Use the schema** to validate your settings object
3. **Check maxLength** to avoid exceeding character limits
4. **For AI agents**: Cache the settings to avoid repeated API calls
5. **Required fields** must be included in your settings object
## Comparison: Before vs After
### Before ❌
```bash
# Had to guess what settings are available
# Had to read documentation or source code
# Didn't know character limits
```
### After ✅
```bash
# Discover settings programmatically
postiz integrations:settings reddit-123
# See exactly what's required and optional
# Know the exact character limits
# Get validation schemas
```
## Summary
✅ **Discover settings for any integration**
✅ **Get character limits**
✅ **See validation schemas**
✅ **Know required vs optional fields**
✅ **Perfect for AI agents**
✅ **No more guesswork!**
**Now you can discover what settings each platform supports!** 🎉

View file

@ -1,435 +0,0 @@
# Integration Tools Workflow
Some integrations require additional data (like IDs, tags, playlists, etc.) before you can post. The CLI supports a complete workflow to discover and use these tools.
## The Complete Workflow
### Step 1: List Integrations
```bash
postiz integrations:list
```
Get your integration IDs.
### Step 2: Get Integration Settings
```bash
postiz integrations:settings <integration-id>
```
This returns:
- `maxLength` - Character limit
- `settings` - Required/optional fields
- **`tools`** - Callable methods to fetch additional data
### Step 3: Trigger Tools (If Needed)
If settings require IDs/data you don't have, use the tools:
```bash
postiz integrations:trigger <integration-id> <method-name> -d '{"key":"value"}'
```
### Step 4: Create Post with Complete Settings
Use the data from Step 3 in your post settings.
## Real-World Example: Reddit
### 1. Get Reddit Integration Settings
```bash
postiz integrations:settings reddit-abc123
```
**Output:**
```json
{
"output": {
"maxLength": 40000,
"settings": {
"properties": {
"subreddit": {
"type": "array",
"items": {
"properties": {
"subreddit": { "type": "string" },
"title": { "type": "string" },
"flair": {
"properties": {
"id": { "type": "string" } // ← Need flair ID!
}
}
}
}
}
}
},
"tools": [
{
"methodName": "getFlairs",
"description": "Get available flairs for a subreddit",
"dataSchema": [
{
"key": "subreddit",
"description": "The subreddit name",
"type": "string"
}
]
},
{
"methodName": "searchSubreddits",
"description": "Search for subreddits",
"dataSchema": [
{
"key": "query",
"description": "Search query",
"type": "string"
}
]
}
]
}
}
```
### 2. Get Flairs for the Subreddit
```bash
postiz integrations:trigger reddit-abc123 getFlairs -d '{"subreddit":"programming"}'
```
**Output:**
```json
{
"output": [
{
"id": "flair-12345",
"name": "Discussion"
},
{
"id": "flair-67890",
"name": "Tutorial"
}
]
}
```
### 3. Create Post with Flair ID
```bash
postiz posts:create \
-c "Check out my project!" \
-p reddit \
--settings '{
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "My Cool Project",
"type": "text",
"url": "",
"is_flair_required": true,
"flair": {
"id": "flair-12345",
"name": "Discussion"
}
}
}]
}' \
-i "reddit-abc123"
```
## Example: YouTube Playlists
### 1. Get YouTube Settings
```bash
postiz integrations:settings youtube-123
```
**Output includes tools:**
```json
{
"tools": [
{
"methodName": "getPlaylists",
"description": "Get your YouTube playlists",
"dataSchema": []
},
{
"methodName": "getCategories",
"description": "Get available video categories",
"dataSchema": []
}
]
}
```
### 2. Get Playlists
```bash
postiz integrations:trigger youtube-123 getPlaylists
```
**Output:**
```json
{
"output": [
{
"id": "PLxxxxxx",
"title": "My Tutorials"
},
{
"id": "PLyyyyyy",
"title": "Product Demos"
}
]
}
```
### 3. Post to Specific Playlist
```bash
postiz posts:create \
-c "Video description" \
-p youtube \
--settings '{
"title": "My Video",
"type": "public",
"playlistId": "PLxxxxxx"
}' \
-i "youtube-123"
```
## Example: LinkedIn Companies
### 1. Get LinkedIn Settings
```bash
postiz integrations:settings linkedin-123
```
**Output includes tools:**
```json
{
"tools": [
{
"methodName": "getCompanies",
"description": "Get companies you can post to",
"dataSchema": []
}
]
}
```
### 2. Get Companies
```bash
postiz integrations:trigger linkedin-123 getCompanies
```
**Output:**
```json
{
"output": [
{
"id": "company-123",
"name": "My Company"
},
{
"id": "company-456",
"name": "Other Company"
}
]
}
```
### 3. Post as Company
```bash
postiz posts:create \
-c "Company announcement" \
-p linkedin \
--settings '{
"companyId": "company-123"
}' \
-i "linkedin-123"
```
## Understanding Tools
### Tool Structure
```json
{
"methodName": "getFlairs",
"description": "Get available flairs for a subreddit",
"dataSchema": [
{
"key": "subreddit",
"description": "The subreddit name",
"type": "string"
}
]
}
```
- **methodName** - Use this in `integrations:trigger`
- **description** - What the tool does
- **dataSchema** - Required input parameters
### Calling Tools
```bash
# No parameters
postiz integrations:trigger <integration-id> <methodName>
# With parameters
postiz integrations:trigger <integration-id> <methodName> -d '{"key":"value"}'
```
## Common Tool Methods
### Reddit
- `getFlairs` - Get flairs for a subreddit
- `searchSubreddits` - Search for subreddits
- `getSubreddits` - Get subscribed subreddits
### YouTube
- `getPlaylists` - Get your playlists
- `getCategories` - Get video categories
- `getChannels` - Get your channels
### LinkedIn
- `getCompanies` - Get companies you manage
- `getOrganizations` - Get organizations
### Twitter/X
- `getListsowned` - Get your Twitter lists
- `getCommunities` - Get communities you're in
### Pinterest
- `getBoards` - Get your Pinterest boards
- `getBoardSections` - Get sections in a board
## AI Agent Workflow
For AI agents, this enables dynamic discovery and usage:
```javascript
// 1. Get settings and tools
const settings = JSON.parse(
execSync(`postiz integrations:settings ${integrationId}`)
);
// 2. Check if tools are needed
const tools = settings.output.tools || [];
// 3. Call tools to get required data
for (const tool of tools) {
if (needsThisTool(tool)) {
const data = buildDataForTool(tool.dataSchema);
const result = JSON.parse(
execSync(
`postiz integrations:trigger ${integrationId} ${tool.methodName} -d '${JSON.stringify(data)}'`
)
);
// Use result.output in your settings
updateSettings(result.output);
}
}
// 4. Create post with complete settings
execSync(`postiz posts:create -c "${content}" --settings '${JSON.stringify(settings)}' -i "${integrationId}"`);
```
## Error Handling
### Tool Not Found
```bash
postiz integrations:trigger reddit-123 invalidMethod
# ❌ Failed to trigger tool: Tool not found
```
### Missing Required Data
```bash
postiz integrations:trigger reddit-123 getFlairs
# ❌ Missing required parameter: subreddit
```
### Integration Not Found
```bash
postiz integrations:trigger invalid-id getFlairs
# ❌ Failed to trigger tool: Integration not found
```
## Tips
1. **Always check tools first** - Run `integrations:settings` to see available tools
2. **Read dataSchema** - Know what parameters each tool needs
3. **Parse JSON output** - Use `jq` or similar to extract data
4. **Cache results** - Tool results don't change often
5. **For AI agents** - Automate the entire workflow
## Complete Example Script
```bash
#!/bin/bash
export POSTIZ_API_KEY=your_key
INTEGRATION_ID="reddit-abc123"
# 1. Get settings
echo "📋 Getting settings..."
SETTINGS=$(postiz integrations:settings $INTEGRATION_ID)
echo $SETTINGS | jq '.output.tools'
# 2. Get flairs
echo ""
echo "🏷️ Getting flairs..."
FLAIRS=$(postiz integrations:trigger $INTEGRATION_ID getFlairs -d '{"subreddit":"programming"}')
FLAIR_ID=$(echo $FLAIRS | jq -r '.output[0].id')
FLAIR_NAME=$(echo $FLAIRS | jq -r '.output[0].name')
echo "Selected flair: $FLAIR_NAME ($FLAIR_ID)"
# 3. Create post
echo ""
echo "📝 Creating post..."
postiz posts:create \
-c "My post content" \
-p reddit \
--settings "{
\"subreddit\": [{
\"value\": {
\"subreddit\": \"programming\",
\"title\": \"My Post Title\",
\"type\": \"text\",
\"url\": \"\",
\"is_flair_required\": true,
\"flair\": {
\"id\": \"$FLAIR_ID\",
\"name\": \"$FLAIR_NAME\"
}
}
}]
}" \
-i "$INTEGRATION_ID"
echo "✅ Done!"
```
## Summary
**Discover available tools** with `integrations:settings`
**Call tools** to fetch required data with `integrations:trigger`
**Use tool results** in post settings
**Complete workflow** from discovery to posting
**Perfect for AI agents** - fully automated
**No guesswork** - know exactly what data you need
**The CLI now supports the complete integration tools workflow!** 🎉

View file

@ -1,338 +0,0 @@
# Postiz CLI - Project Structure
## Overview
The Postiz CLI is a complete command-line interface package for interacting with the Postiz social media scheduling API. It's designed for developers and AI agents to automate social media posting.
## Directory Structure
```
apps/cli/
├── src/ # Source code
│ ├── index.ts # Main CLI entry point
│ ├── api.ts # API client for Postiz API
│ ├── config.ts # Configuration and environment handling
│ └── commands/ # Command implementations
│ ├── posts.ts # Posts management commands
│ ├── integrations.ts # Integrations listing
│ └── upload.ts # Media upload command
├── examples/ # Usage examples
│ ├── basic-usage.sh # Shell script example
│ └── ai-agent-example.js # Node.js AI agent example
├── dist/ # Build output (generated)
│ ├── index.js # Compiled CLI executable
│ └── index.js.map # Source map
├── package.json # Package configuration
├── tsconfig.json # TypeScript configuration
├── tsup.config.ts # Build configuration
├── README.md # Main documentation
├── SKILL.md # AI agent usage guide
├── QUICK_START.md # Quick start guide
├── CHANGELOG.md # Version history
├── PROJECT_STRUCTURE.md # This file
├── .gitignore # Git ignore rules
└── .npmignore # npm publish ignore rules
```
## File Descriptions
### Source Files
#### `src/index.ts`
- Main entry point for the CLI
- Uses `yargs` for command parsing
- Defines all available commands and their options
- Contains help text and usage examples
#### `src/api.ts`
- API client class `PostizAPI`
- Handles all HTTP requests to the Postiz API
- Methods for:
- Creating posts
- Listing posts
- Deleting posts
- Uploading files
- Listing integrations
- Error handling and response parsing
#### `src/config.ts`
- Configuration management
- Environment variable handling
- Validates required settings (API key)
- Provides default values
#### `src/commands/posts.ts`
- Post management commands implementation
- `createPost()` - Create new social media posts
- `listPosts()` - List posts with filters
- `deletePost()` - Delete posts by ID
#### `src/commands/integrations.ts`
- Integration management
- `listIntegrations()` - Show connected accounts
#### `src/commands/upload.ts`
- Media upload functionality
- `uploadFile()` - Upload images to Postiz
### Configuration Files
#### `package.json`
- Package name: `postiz`
- Version: `1.0.0`
- Executable bin: `postiz``dist/index.js`
- Scripts: `dev`, `build`, `start`, `publish`
- Repository and metadata information
#### `tsconfig.json`
- Extends base config from monorepo
- Target: ES2017
- Module: CommonJS
- Enables decorators and source maps
#### `tsup.config.ts`
- Build tool configuration
- Entry point: `src/index.ts`
- Output format: CommonJS
- Adds shebang for Node.js execution
- Generates source maps
### Documentation Files
#### `README.md`
- Main package documentation
- Installation instructions
- Usage examples
- API reference
- Development guide
#### `SKILL.md`
- Comprehensive guide for AI agents
- Usage patterns and workflows
- Command examples
- Best practices
- Error handling
#### `QUICK_START.md`
- Fast onboarding guide
- Installation steps
- Basic commands
- Common workflows
- Troubleshooting
#### `CHANGELOG.md`
- Version history
- Release notes
- Feature additions
- Bug fixes
### Example Files
#### `examples/basic-usage.sh`
- Bash script example
- Demonstrates basic CLI workflow
- Shows integration listing, post creation, and deletion
#### `examples/ai-agent-example.js`
- Node.js script for AI agents
- Programmatic CLI usage
- Batch post creation
- JSON parsing examples
## Build Process
### Development Build
```bash
pnpm run dev
```
- Watches for file changes
- Rebuilds automatically
- Useful during development
### Production Build
```bash
pnpm run build
```
1. Cleans `dist/` directory
2. Compiles TypeScript → JavaScript
3. Bundles dependencies
4. Adds shebang for executable
5. Generates source maps
6. Makes output executable
### Output
- `dist/index.js` - Main executable (~490KB)
- `dist/index.js.map` - Source map (~920KB)
## Commands Architecture
### Command Flow
```
User Input
index.ts (yargs parser)
Command Handler (posts.ts, integrations.ts, upload.ts)
config.ts (get API key)
api.ts (make API request)
Response / Error
Output to console
```
### Available Commands
1. **posts:create**
- Options: `--content`, `--integrations`, `--schedule`, `--image`
- Handler: `commands/posts.ts::createPost()`
2. **posts:list**
- Options: `--page`, `--limit`, `--search`
- Handler: `commands/posts.ts::listPosts()`
3. **posts:delete**
- Positional: `<id>`
- Handler: `commands/posts.ts::deletePost()`
4. **integrations:list**
- No options
- Handler: `commands/integrations.ts::listIntegrations()`
5. **upload**
- Positional: `<file>`
- Handler: `commands/upload.ts::uploadFile()`
## Environment Variables
| Variable | Required | Default | Usage |
|----------|----------|---------|-------|
| `POSTIZ_API_KEY` | ✅ Yes | - | Authentication token |
| `POSTIZ_API_URL` | ❌ No | `https://api.postiz.com` | Custom API endpoint |
## Dependencies
### Runtime Dependencies (from root)
- `yargs` - CLI argument parsing
- `node-fetch` - HTTP requests
- Standard Node.js modules (`fs`, `path`)
### Dev Dependencies
- `tsup` - TypeScript bundler
- `typescript` - Type checking
- `@types/yargs` - TypeScript types
## Integration Points
### With Monorepo
1. **Build Scripts**
- Added to root `package.json`
- `pnpm run build:cli` - Build the CLI
- `pnpm run publish-cli` - Publish to npm
2. **TypeScript Config**
- Extends `tsconfig.base.json`
- Shares common compiler options
3. **Dependencies**
- Uses shared dependencies from root
- No duplicate packages
### With Postiz API
1. **Endpoints Used**
- `POST /public/v1/posts` - Create post
- `GET /public/v1/posts` - List posts
- `DELETE /public/v1/posts/:id` - Delete post
- `GET /public/v1/integrations` - List integrations
- `POST /public/v1/upload` - Upload media
2. **Authentication**
- API key via `Authorization` header
- Configured through environment variable
## Publishing
### To npm
```bash
pnpm run publish-cli
```
This will:
1. Build the package
2. Publish to npm with public access
3. Include only `dist/`, `README.md`, and `SKILL.md`
### Package Contents (via .npmignore)
**Included:**
- `dist/` - Compiled code
- `README.md` - Documentation
**Excluded:**
- `src/` - Source code
- `examples/` - Examples
- Config files
- Other markdown files
## Testing
### Manual Testing
```bash
# Test help
node dist/index.js --help
# Test without API key (should error)
node dist/index.js posts:list
# Test with API key (requires valid key)
POSTIZ_API_KEY=test node dist/index.js integrations:list
```
### Automated Testing (Future)
- Unit tests for API client
- Integration tests for commands
- E2E tests with mock API
## Future Enhancements
1. **More Commands**
- Analytics retrieval
- Team management
- Settings configuration
2. **Features**
- Interactive mode
- Config file support (~/.postizrc)
- Output formatting (JSON, table, CSV)
- Verbose/debug mode
- Batch operations from file
3. **Developer Experience**
- TypeScript types export
- Programmatic API
- Plugin system
- Custom integrations
## Support
- **Issues:** https://github.com/gitroomhq/postiz-app/issues
- **Docs:** See README.md, SKILL.md, QUICK_START.md
- **Website:** https://postiz.com

View file

@ -1,472 +0,0 @@
# Provider-Specific Settings
The Postiz CLI supports platform-specific settings for each integration. Different platforms have different options and requirements.
## How to Use Provider Settings
### Method 1: Command Line Flags
```bash
postiz posts:create \
-c "Your content" \
-p <provider-type> \
--settings '<json-settings>' \
-i "integration-id"
```
### Method 2: JSON File
```bash
postiz posts:create --json post-with-settings.json
```
In the JSON file, specify settings per integration:
```json
{
"type": "now",
"date": "2024-01-15T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [{
"integration": { "id": "reddit-123" },
"value": [{ "content": "Post content", "image": [] }],
"settings": {
"__type": "reddit",
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "My Post Title",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}
}]
}
```
## Supported Platforms & Settings
### Reddit (`reddit`)
**Settings:**
- `subreddit` (required): Subreddit name
- `title` (required): Post title
- `type` (required): `"text"` or `"link"`
- `url` (required for links): URL if type is "link"
- `is_flair_required` (boolean): Whether flair is required
- `flair` (optional): Flair object with `id` and `name`
**Example:**
```bash
postiz posts:create \
-c "Post content here" \
-p reddit \
--settings '{
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "Check out this cool project",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}' \
-i "reddit-123"
```
### YouTube (`youtube`)
**Settings:**
- `title` (required): Video title (2-100 characters)
- `type` (required): `"public"`, `"private"`, or `"unlisted"`
- `selfDeclaredMadeForKids` (optional): `"yes"` or `"no"`
- `thumbnail` (optional): Thumbnail MediaDto object
- `tags` (optional): Array of tag objects with `value` and `label`
**Example:**
```bash
postiz posts:create \
-c "Video description here" \
-p youtube \
--settings '{
"title": "My Awesome Video",
"type": "public",
"selfDeclaredMadeForKids": "no",
"tags": [
{"value": "tech", "label": "Tech"},
{"value": "tutorial", "label": "Tutorial"}
]
}' \
-i "youtube-123"
```
### X / Twitter (`x`)
**Settings:**
- `community` (optional): X community URL (format: `https://x.com/i/communities/1234567890`)
- `who_can_reply_post` (required): Who can reply
- `"everyone"` - Anyone can reply
- `"following"` - Only people you follow
- `"mentionedUsers"` - Only mentioned users
- `"subscribers"` - Only subscribers
- `"verified"` - Only verified users
**Example:**
```bash
postiz posts:create \
-c "Tweet content" \
-p x \
--settings '{
"who_can_reply_post": "everyone"
}' \
-i "twitter-123"
```
**With Community:**
```bash
postiz posts:create \
-c "Community tweet" \
-p x \
--settings '{
"community": "https://x.com/i/communities/1493446837214187523",
"who_can_reply_post": "everyone"
}' \
-i "twitter-123"
```
### LinkedIn (`linkedin`)
**Settings:**
- `post_as_images_carousel` (boolean): Post as image carousel
- `carousel_name` (optional): Carousel name if posting as carousel
**Example:**
```bash
postiz posts:create \
-c "LinkedIn post" \
-m "img1.jpg,img2.jpg,img3.jpg" \
-p linkedin \
--settings '{
"post_as_images_carousel": true,
"carousel_name": "Product Showcase"
}' \
-i "linkedin-123"
```
### Instagram (`instagram`)
**Settings:**
- `post_type` (required): `"post"` or `"story"`
- `is_trial_reel` (optional): Boolean
- `graduation_strategy` (optional): `"MANUAL"` or `"SS_PERFORMANCE"`
- `collaborators` (optional): Array of collaborator objects with `label`
**Example:**
```bash
postiz posts:create \
-c "Instagram post" \
-m "photo.jpg" \
-p instagram \
--settings '{
"post_type": "post",
"is_trial_reel": false
}' \
-i "instagram-123"
```
**Story Example:**
```bash
postiz posts:create \
-c "Story content" \
-m "story-image.jpg" \
-p instagram \
--settings '{
"post_type": "story"
}' \
-i "instagram-123"
```
### TikTok (`tiktok`)
**Settings:**
- `title` (optional): Video title (max 90 characters)
- `privacy_level` (required): Privacy level
- `"PUBLIC_TO_EVERYONE"`
- `"MUTUAL_FOLLOW_FRIENDS"`
- `"FOLLOWER_OF_CREATOR"`
- `"SELF_ONLY"`
- `duet` (boolean): Allow duets
- `stitch` (boolean): Allow stitch
- `comment` (boolean): Allow comments
- `autoAddMusic` (required): `"yes"` or `"no"`
- `brand_content_toggle` (boolean): Brand content toggle
- `brand_organic_toggle` (boolean): Brand organic toggle
- `video_made_with_ai` (optional): Boolean
- `content_posting_method` (required): `"DIRECT_POST"` or `"UPLOAD"`
**Example:**
```bash
postiz posts:create \
-c "TikTok video description" \
-m "video.mp4" \
-p tiktok \
--settings '{
"title": "Check this out!",
"privacy_level": "PUBLIC_TO_EVERYONE",
"duet": true,
"stitch": true,
"comment": true,
"autoAddMusic": "no",
"brand_content_toggle": false,
"brand_organic_toggle": false,
"content_posting_method": "DIRECT_POST"
}' \
-i "tiktok-123"
```
### Facebook (`facebook`)
Settings available - check the DTO for specifics.
### Pinterest (`pinterest`)
Settings available - check the DTO for specifics.
### Discord (`discord`)
Settings available - check the DTO for specifics.
### Slack (`slack`)
Settings available - check the DTO for specifics.
### Medium (`medium`)
Settings available - check the DTO for specifics.
### Dev.to (`devto`)
Settings available - check the DTO for specifics.
### Hashnode (`hashnode`)
Settings available - check the DTO for specifics.
### WordPress (`wordpress`)
Settings available - check the DTO for specifics.
## Platforms Without Specific Settings
These platforms use the default `EmptySettings`:
- `threads`
- `mastodon`
- `bluesky`
- `telegram`
- `nostr`
- `vk`
For these, you don't need to specify settings or can use:
```bash
-p threads # or any of the above
```
## Using JSON Files for Complex Settings
For complex settings, it's easier to use JSON files:
### Reddit Example
**reddit-post.json:**
```json
{
"type": "now",
"date": "2024-01-15T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [{
"integration": { "id": "reddit-123" },
"value": [{
"content": "Check out this cool project!",
"image": []
}],
"settings": {
"__type": "reddit",
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "My Cool Project - Built with TypeScript",
"type": "text",
"url": "",
"is_flair_required": true,
"flair": {
"id": "flair-123",
"name": "Project"
}
}
}]
}
}]
}
```
```bash
postiz posts:create --json reddit-post.json
```
### YouTube Example
**youtube-video.json:**
```json
{
"type": "schedule",
"date": "2024-12-25T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [{
"integration": { "id": "youtube-123" },
"value": [{
"content": "Full video description with timestamps...",
"image": [{
"id": "thumb1",
"path": "https://cdn.example.com/thumbnail.jpg"
}]
}],
"settings": {
"__type": "youtube",
"title": "How to Build a CLI Tool",
"type": "public",
"selfDeclaredMadeForKids": "no",
"tags": [
{ "value": "programming", "label": "Programming" },
{ "value": "typescript", "label": "TypeScript" },
{ "value": "tutorial", "label": "Tutorial" }
]
}
}]
}
```
```bash
postiz posts:create --json youtube-video.json
```
### Multi-Platform with Different Settings
**multi-platform-campaign.json:**
```json
{
"type": "now",
"date": "2024-01-15T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [
{
"integration": { "id": "reddit-123" },
"value": [{ "content": "Reddit-specific content", "image": [] }],
"settings": {
"__type": "reddit",
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "Post Title",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}
},
{
"integration": { "id": "twitter-123" },
"value": [{ "content": "Twitter-specific content", "image": [] }],
"settings": {
"__type": "x",
"who_can_reply_post": "everyone"
}
},
{
"integration": { "id": "linkedin-123" },
"value": [
{
"content": "LinkedIn post",
"image": [
{ "id": "1", "path": "img1.jpg" },
{ "id": "2", "path": "img2.jpg" }
]
}
],
"settings": {
"__type": "linkedin",
"post_as_images_carousel": true,
"carousel_name": "Product Launch"
}
}
]
}
```
## Tips
1. **Use JSON files for complex settings** - Command-line JSON strings get messy fast
2. **Validate your settings** - The API will return errors if settings are invalid
3. **Check required fields** - Each platform has different required fields
4. **Platform-specific content** - Different platforms may need different content/media
5. **Test with drafts first** - Use `"type": "draft"` to test without posting
## Finding Your Provider Type
To find the correct provider type for your integration:
```bash
postiz integrations:list
```
This will show the `provider` field for each integration, which corresponds to the `__type` in settings.
## Common Errors
### Missing __type
```json
{
"settings": {
"title": "My Video" // ❌ Missing __type
}
}
```
**Fix:**
```json
{
"settings": {
"__type": "youtube", // ✅ Add __type
"title": "My Video"
}
}
```
### Wrong Provider Type
```bash
# ❌ Wrong
-p twitter # Should be "x"
# ✅ Correct
-p x
```
### Invalid Settings for Platform
Each platform validates its own settings. Check the error message and refer to the platform's required fields above.
## See Also
- **EXAMPLES.md** - General usage examples
- **COMMAND_LINE_GUIDE.md** - Command-line syntax
- **SKILL.md** - AI agent patterns
- Source DTOs in `libraries/nestjs-libraries/src/dtos/posts/providers-settings/`

View file

@ -1,220 +0,0 @@
# Provider-Specific Settings - Quick Reference
## ✅ What's Supported
The CLI now supports **platform-specific settings** for all 28+ integrations!
## Supported Platforms
### Platforms with Specific Settings
| Platform | Type | Key Settings |
|----------|------|--------------|
| **Reddit** | `reddit` | subreddit, title, type, url, flair |
| **YouTube** | `youtube` | title, type (public/private/unlisted), tags, thumbnail |
| **X (Twitter)** | `x` | who_can_reply_post, community |
| **LinkedIn** | `linkedin` | post_as_images_carousel, carousel_name |
| **Instagram** | `instagram` | post_type (post/story), collaborators |
| **TikTok** | `tiktok` | title, privacy_level, duet, stitch, comment, autoAddMusic |
| **Facebook** | `facebook` | Platform-specific settings |
| **Pinterest** | `pinterest` | Platform-specific settings |
| **Discord** | `discord` | Platform-specific settings |
| **Slack** | `slack` | Platform-specific settings |
| **Medium** | `medium` | Platform-specific settings |
| **Dev.to** | `devto` | Platform-specific settings |
| **Hashnode** | `hashnode` | Platform-specific settings |
| **WordPress** | `wordpress` | Platform-specific settings |
| And 15+ more... | | See PROVIDER_SETTINGS.md |
### Platforms with Default Settings
These use `EmptySettings` (no special configuration needed):
- Threads, Mastodon, Bluesky, Telegram, Nostr, VK
## Usage
### Method 1: Command Line
```bash
postiz posts:create \
-c "Content" \
-p <provider-type> \
--settings '<json-settings>' \
-i "integration-id"
```
### Method 2: JSON File
```json
{
"posts": [{
"integration": { "id": "integration-id" },
"value": [...],
"settings": {
"__type": "provider-type",
...
}
}]
}
```
## Quick Examples
### Reddit Post
```bash
postiz posts:create \
-c "Check out this project!" \
-p reddit \
--settings '{
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "My Cool Project",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}' \
-i "reddit-123"
```
### YouTube Video
```bash
postiz posts:create \
-c "Full video description..." \
-p youtube \
--settings '{
"title": "How to Build a CLI",
"type": "public",
"tags": [
{"value": "tech", "label": "Tech"},
{"value": "tutorial", "label": "Tutorial"}
]
}' \
-i "youtube-123"
```
### Twitter/X with Reply Controls
```bash
postiz posts:create \
-c "Important announcement!" \
-p x \
--settings '{
"who_can_reply_post": "verified"
}' \
-i "twitter-123"
```
### LinkedIn Carousel
```bash
postiz posts:create \
-c "Product showcase" \
-m "img1.jpg,img2.jpg,img3.jpg" \
-p linkedin \
--settings '{
"post_as_images_carousel": true,
"carousel_name": "Product Launch"
}' \
-i "linkedin-123"
```
### Instagram Story
```bash
postiz posts:create \
-c "Story content" \
-m "story-image.jpg" \
-p instagram \
--settings '{
"post_type": "story"
}' \
-i "instagram-123"
```
### TikTok Video
```bash
postiz posts:create \
-c "TikTok description #fyp" \
-m "video.mp4" \
-p tiktok \
--settings '{
"privacy_level": "PUBLIC_TO_EVERYONE",
"duet": true,
"stitch": true,
"comment": true,
"autoAddMusic": "no",
"brand_content_toggle": false,
"brand_organic_toggle": false,
"content_posting_method": "DIRECT_POST"
}' \
-i "tiktok-123"
```
## JSON File Examples
We've created example JSON files for you:
- **`reddit-post.json`** - Reddit post with subreddit settings
- **`youtube-video.json`** - YouTube video with title, tags, thumbnail
- **`tiktok-video.json`** - TikTok video with full settings
- **`multi-platform-with-settings.json`** - Multi-platform campaign with different settings per platform
## Finding Provider Types
```bash
postiz integrations:list
```
Look at the `provider` field - this is your provider type!
## Common Provider Types
- `reddit` - Reddit
- `youtube` - YouTube
- `x` - X (Twitter)
- `linkedin` or `linkedin-page` - LinkedIn
- `instagram` or `instagram-standalone` - Instagram
- `tiktok` - TikTok
- `facebook` - Facebook
- `pinterest` - Pinterest
- `discord` - Discord
- `slack` - Slack
- `threads` - Threads (no specific settings)
- `bluesky` - Bluesky (no specific settings)
- `mastodon` - Mastodon (no specific settings)
## Documentation
📖 **[PROVIDER_SETTINGS.md](./PROVIDER_SETTINGS.md)** - Complete documentation with all platform settings
Includes:
- All available settings for each platform
- Required vs optional fields
- Validation rules
- More examples
- Common errors and solutions
## Tips
1. **Use JSON files for complex settings** - Easier to manage than command-line strings
2. **Different settings per platform** - Each platform in a multi-platform post can have different settings
3. **Validate before posting** - Use `"type": "draft"` to test
4. **Check examples** - See `examples/` directory for working templates
5. **Provider type matters** - Make sure `__type` matches your integration's provider
## Summary
✅ **28+ platforms supported**
✅ **Platform-specific settings for Reddit, YouTube, TikTok, X, LinkedIn, Instagram, and more**
✅ **Easy command-line interface**
✅ **JSON file support for complex configs**
✅ **Full type validation**
✅ **Comprehensive examples included**
**The CLI now supports the full power of each platform!** 🚀

View file

@ -1,377 +0,0 @@
# Publishing the Postiz CLI to npm
## Quick Publish (Current Name: "postiz")
```bash
# From apps/cli directory
pnpm run build
pnpm publish --access public
```
Then users can install:
```bash
npm install -g postiz
# or
pnpm install -g postiz
# And use:
postiz --help
```
## Publishing with a Different Package Name
If you want to publish as a different npm package name (e.g., "agent-postiz"):
### 1. Change Package Name
Edit `apps/cli/package.json`:
```json
{
"name": "agent-postiz", // ← Changed package name
"version": "1.0.0",
"bin": {
"postiz": "./dist/index.js" // ← Keep command name!
}
}
```
**Important:** The `bin` field determines the command name, NOT the package name!
### 2. Publish
```bash
cd apps/cli
pnpm run build
pnpm publish --access public
```
### 3. Users Install
```bash
npm install -g agent-postiz
# or
pnpm install -g agent-postiz
```
### 4. Users Use
Even though the package is called "agent-postiz", the command is still:
```bash
postiz --help # ← Command name from "bin" field
postiz posts:create -c "Hello!" -i "twitter-123"
```
## Package Name vs Command Name
| Field | Purpose | Example |
|-------|---------|---------|
| `"name"` | npm package name (what you install) | `"agent-postiz"` |
| `"bin"` | Command name (what you type) | `"postiz"` |
**Examples:**
1. **Same name:**
```json
"name": "postiz",
"bin": { "postiz": "./dist/index.js" }
```
Install: `npm i -g postiz`
Use: `postiz`
2. **Different names:**
```json
"name": "agent-postiz",
"bin": { "postiz": "./dist/index.js" }
```
Install: `npm i -g agent-postiz`
Use: `postiz`
3. **Multiple commands:**
```json
"name": "agent-postiz",
"bin": {
"postiz": "./dist/index.js",
"pz": "./dist/index.js"
}
```
Install: `npm i -g agent-postiz`
Use: `postiz` or `pz`
## Publishing Checklist
### Before First Publish
- [ ] Verify package name is available on npm
```bash
npm view postiz
# If error "404 Not Found" - name is available!
```
- [ ] Update version if needed
```json
"version": "1.0.0"
```
- [ ] Review files to include
```json
"files": [
"dist",
"README.md",
"SKILL.md"
]
```
- [ ] Build the package
```bash
pnpm run build
```
- [ ] Test locally
```bash
pnpm link --global
postiz --help
```
### Publish to npm
```bash
# Login to npm (first time only)
npm login
# From apps/cli
pnpm run build
pnpm publish --access public
# Or use the root script
cd /path/to/monorepo/root
pnpm run publish-cli
```
### After Publishing
Verify it's published:
```bash
npm view postiz
# Should show your package info
```
Test installation:
```bash
npm install -g postiz
postiz --version
```
## Using from Monorepo Root
The root `package.json` already has:
```json
{
"scripts": {
"publish-cli": "pnpm run --filter ./apps/cli publish"
}
}
```
So you can publish from the root:
```bash
# From monorepo root
pnpm run publish-cli
```
## Version Updates
### Patch Release (1.0.0 → 1.0.1)
```bash
cd apps/cli
npm version patch
pnpm publish --access public
```
### Minor Release (1.0.0 → 1.1.0)
```bash
cd apps/cli
npm version minor
pnpm publish --access public
```
### Major Release (1.0.0 → 2.0.0)
```bash
cd apps/cli
npm version major
pnpm publish --access public
```
## Scoped Packages
If you want to publish under an organization scope:
```json
{
"name": "@yourorg/postiz",
"bin": {
"postiz": "./dist/index.js"
}
}
```
Install:
```bash
npm install -g @yourorg/postiz
```
Use:
```bash
postiz --help
```
## Testing Before Publishing
### Test the Build
```bash
pnpm run build
node dist/index.js --help
```
### Test Linking
```bash
pnpm link --global
postiz --help
pnpm unlink --global
```
### Test Publishing (Dry Run)
```bash
npm publish --dry-run
# Shows what would be published
```
### Test with `npm pack`
```bash
npm pack
# Creates a .tgz file
# Test installing the tarball
npm install -g ./postiz-1.0.0.tgz
postiz --help
npm uninstall -g postiz
```
## Continuous Publishing
### Using GitHub Actions
Create `.github/workflows/publish-cli.yml`:
```yaml
name: Publish CLI to npm
on:
push:
tags:
- 'cli-v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install
- run: pnpm run build:cli
- name: Publish to npm
run: pnpm --filter ./apps/cli publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Then publish with:
```bash
git tag cli-v1.0.0
git push origin cli-v1.0.0
```
## Common Issues
### "You do not have permission to publish"
- Make sure you're logged in: `npm login`
- Check package name isn't taken: `npm view postiz`
- If scoped, ensure org access: `npm org ls yourorg`
### "Package name too similar to existing package"
- Choose a more unique name
- Or use a scoped package: `@yourorg/postiz`
### "Missing required files"
- Check `"files"` field in package.json
- Run `npm pack` to see what would be included
- Make sure `dist/` exists and is built
### Command not found after install
- Check `"bin"` field is correct
- Ensure `dist/index.js` has shebang: `#!/usr/bin/env node`
- Try reinstalling: `npm uninstall -g postiz && npm install -g postiz`
## Recommended Names
If "postiz" is taken, consider:
- `@postiz/cli`
- `postiz-cli`
- `postiz-agent`
- `agent-postiz`
- `@yourorg/postiz`
Remember: The package name is just for installation. The command can still be `postiz`!
## Summary
✅ Current setup works perfectly!
`bin` field defines the command name
`name` field defines the npm package name
✅ They can be different!
**To publish now:**
```bash
cd apps/cli
pnpm run build
pnpm publish --access public
```
**Users install:**
```bash
npm install -g postiz
# or
pnpm install -g postiz
```
**Users use:**
```bash
postiz --help
postiz posts:create -c "Hello!" -i "twitter-123"
```
🚀 **Ready to publish!**

View file

@ -1,284 +0,0 @@
# Postiz CLI - Quick Start Guide
## Installation
### From Source (Development)
```bash
# Navigate to the monorepo root
cd /path/to/gitroom
# Install dependencies
pnpm install
# Build the CLI
pnpm run build:cli
# Test locally
node apps/cli/dist/index.js --help
```
### Global Installation (Development)
```bash
# From the CLI directory
cd apps/cli
# Link globally
pnpm link --global
# Now you can use 'postiz' anywhere
postiz --help
```
### From npm (Coming Soon)
```bash
# Once published
npm install -g postiz
# Or with pnpm
pnpm add -g postiz
```
## Setup
### 1. Get Your API Key
1. Log in to your Postiz account at https://postiz.com
2. Navigate to Settings → API Keys
3. Generate a new API key
### 2. Set Environment Variable
```bash
# Bash/Zsh
export POSTIZ_API_KEY=your_api_key_here
# Fish
set -x POSTIZ_API_KEY your_api_key_here
# PowerShell
$env:POSTIZ_API_KEY="your_api_key_here"
```
To make it permanent, add it to your shell profile:
```bash
# ~/.bashrc or ~/.zshrc
echo 'export POSTIZ_API_KEY=your_api_key_here' >> ~/.bashrc
source ~/.bashrc
```
### 3. Verify Installation
```bash
postiz --help
```
## Basic Commands
### Create a Post
```bash
# Simple post
postiz posts:create -c "Hello World!" -i "twitter-123"
# Post with multiple images
postiz posts:create \
-c "Check these out!" \
-m "img1.jpg,img2.jpg" \
-i "twitter-123"
# Post with comments (each can have different media!)
postiz posts:create \
-c "Main post" -m "main.jpg" \
-c "First comment" -m "comment1.jpg" \
-c "Second comment" -m "comment2.jpg" \
-i "twitter-123"
# Scheduled post
postiz posts:create \
-c "Future post" \
-s "2024-12-31T12:00:00Z" \
-i "twitter-123"
```
### List Posts
```bash
# List all posts
postiz posts:list
# With pagination
postiz posts:list -p 2 -l 20
# Search
postiz posts:list -s "keyword"
```
### Delete a Post
```bash
postiz posts:delete abc123xyz
```
### List Integrations
```bash
postiz integrations:list
```
### Upload Media
```bash
postiz upload ./path/to/image.png
```
## Common Workflows
### 1. Check What's Connected
```bash
# See all your connected social media accounts
postiz integrations:list
```
The output will show integration IDs like:
```json
[
{ "id": "twitter-123", "provider": "twitter" },
{ "id": "linkedin-456", "provider": "linkedin" }
]
```
### 2. Create Multi-Platform Post
```bash
# Use the integration IDs from step 1
postiz posts:create \
-c "Posting to multiple platforms!" \
-i "twitter-123,linkedin-456,facebook-789"
```
### 3. Schedule Multiple Posts
```bash
# Morning post
postiz posts:create -c "Good morning!" -s "2024-01-15T09:00:00Z"
# Afternoon post
postiz posts:create -c "Lunch time update!" -s "2024-01-15T12:00:00Z"
# Evening post
postiz posts:create -c "Good night!" -s "2024-01-15T20:00:00Z"
```
### 4. Upload and Post Image
```bash
# First upload the image
postiz upload ./my-image.png
# Copy the URL from the response, then create post
postiz posts:create -c "Check out this image!" --image "url-from-upload"
```
## Tips & Tricks
### Using with jq for JSON Parsing
```bash
# Get just the post IDs
postiz posts:list | jq '.[] | .id'
# Get integration names
postiz integrations:list | jq '.[] | .provider'
```
### Script Automation
```bash
#!/bin/bash
# Create a batch of posts
for hour in 09 12 15 18; do
postiz posts:create \
-c "Automated post at ${hour}:00" \
-s "2024-01-15T${hour}:00:00Z"
echo "Created post for ${hour}:00"
done
```
### Environment Variables
```bash
# Custom API endpoint (for self-hosted)
export POSTIZ_API_URL=https://your-instance.com
# Use the CLI with custom endpoint
postiz posts:list
```
## Troubleshooting
### API Key Not Set
```
❌ Error: POSTIZ_API_KEY environment variable is required
```
**Solution:** Set the environment variable:
```bash
export POSTIZ_API_KEY=your_key
```
### Command Not Found
```
postiz: command not found
```
**Solution:** Either:
1. Use the full path: `node apps/cli/dist/index.js`
2. Link globally: `cd apps/cli && pnpm link --global`
3. Add to PATH: `export PATH=$PATH:/path/to/apps/cli/dist`
### API Errors
```
❌ API Error (401): Unauthorized
```
**Solution:** Check your API key is valid and has proper permissions.
```
❌ API Error (404): Not Found
```
**Solution:** Verify the post ID exists when deleting.
## Getting Help
```bash
# General help
postiz --help
# Command-specific help
postiz posts:create --help
postiz posts:list --help
postiz posts:delete --help
```
## Next Steps
- Read the full [README.md](./README.md) for detailed documentation
- Check [SKILL.md](./SKILL.md) for AI agent integration patterns
- See [examples/](./examples/) for more usage examples
## Links
- [Postiz Website](https://postiz.com)
- [API Documentation](https://postiz.com/api-docs)
- [GitHub Repository](https://github.com/gitroomhq/postiz-app)
- [Report Issues](https://github.com/gitroomhq/postiz-app/issues)

View file

@ -1,643 +0,0 @@
# Postiz CLI
**Social media automation CLI for AI agents** - Schedule posts across 28+ platforms programmatically.
The Postiz CLI provides a command-line interface to the Postiz API, enabling developers and AI agents to automate social media posting, manage content, and handle media uploads across platforms like Twitter/X, LinkedIn, Reddit, YouTube, TikTok, Instagram, Facebook, and more.
---
## Installation
### From npm (Recommended)
```bash
npm install -g postiz
# or
pnpm install -g postiz
```
### From Source
```bash
git clone https://github.com/gitroomhq/postiz-app.git
cd postiz-app/apps/cli
pnpm install
pnpm run build
pnpm link --global
```
### For Development
```bash
cd apps/cli
pnpm install
pnpm run build
pnpm link --global
# Or run directly without linking
pnpm run start -- posts:list
```
---
## Setup
**Required:** Set your Postiz API key
```bash
export POSTIZ_API_KEY=your_api_key_here
```
**Optional:** Custom API endpoint
```bash
export POSTIZ_API_URL=https://your-custom-api.com
```
---
## Commands
### Discovery & Settings
**List all connected integrations**
```bash
postiz integrations:list
```
Returns integration IDs, provider names, and metadata.
**Get integration settings schema**
```bash
postiz integrations:settings <integration-id>
```
Returns character limits, required settings, and available tools for fetching dynamic data.
**Trigger integration tools**
```bash
postiz integrations:trigger <integration-id> <method-name>
postiz integrations:trigger <integration-id> <method-name> -d '{"key":"value"}'
```
Fetch dynamic data like Reddit flairs, YouTube playlists, LinkedIn companies, etc.
**Examples:**
```bash
# Get Reddit flairs
postiz integrations:trigger reddit-123 getFlairs -d '{"subreddit":"programming"}'
# Get YouTube playlists
postiz integrations:trigger youtube-456 getPlaylists
# Get LinkedIn companies
postiz integrations:trigger linkedin-789 getCompanies
```
---
### Creating Posts
**Simple scheduled post**
```bash
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -i "integration-id"
```
**Draft post**
```bash
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -t draft -i "integration-id"
```
**Post with media**
```bash
postiz posts:create -c "Content" -m "img1.jpg,img2.jpg" -s "2024-12-31T12:00:00Z" -i "integration-id"
```
**Post with comments** (each comment can have its own media)
```bash
postiz posts:create \
-c "Main post" -m "main.jpg" \
-c "First comment" -m "comment1.jpg" \
-c "Second comment" -m "comment2.jpg,comment3.jpg" \
-s "2024-12-31T12:00:00Z" \
-i "integration-id"
```
**Multi-platform post**
```bash
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -i "twitter-id,linkedin-id,facebook-id"
```
**Platform-specific settings**
```bash
postiz posts:create \
-c "Content" \
-s "2024-12-31T12:00:00Z" \
--settings '{"subreddit":[{"value":{"subreddit":"programming","title":"Post Title","type":"text"}}]}' \
-i "reddit-id"
```
**Complex post from JSON file**
```bash
postiz posts:create --json post.json
```
**Options:**
- `-c, --content` - Post/comment content (use multiple times for posts with comments)
- `-s, --date` - Schedule date in ISO 8601 format (REQUIRED)
- `-t, --type` - Post type: "schedule" or "draft" (default: "schedule")
- `-m, --media` - Comma-separated media URLs for corresponding `-c`
- `-i, --integrations` - Comma-separated integration IDs (required)
- `-d, --delay` - Delay between comments in milliseconds (default: 5000)
- `--settings` - Platform-specific settings as JSON string
- `-j, --json` - Path to JSON file with full post structure
- `--shortLink` - Use short links (default: true)
---
### Managing Posts
**List posts**
```bash
postiz posts:list
postiz posts:list --startDate "2024-01-01T00:00:00Z" --endDate "2024-12-31T23:59:59Z"
postiz posts:list --customer "customer-id"
```
Defaults to last 30 days to next 30 days if dates not specified.
**Delete post**
```bash
postiz posts:delete <post-id>
```
---
### Media Upload
**Upload file and get URL**
```bash
postiz upload <file-path>
```
**⚠️ IMPORTANT: Upload Files Before Posting**
You **must** upload media files to Postiz before using them in posts. Many platforms (especially TikTok, Instagram, and YouTube) require verified/trusted URLs and will reject external links.
**Workflow:**
1. Upload your file using `postiz upload`
2. Extract the returned URL
3. Use that URL in your post's `-m` parameter
**Supported formats:**
- **Images:** PNG, JPG, JPEG, GIF, WEBP, SVG, BMP, ICO
- **Videos:** MP4, MOV, AVI, MKV, WEBM, FLV, WMV, M4V, MPEG, MPG, 3GP
- **Audio:** MP3, WAV, OGG, AAC, FLAC, M4A
- **Documents:** PDF, DOC, DOCX
**Example:**
```bash
# 1. Upload the file first
RESULT=$(postiz upload video.mp4)
PATH=$(echo "$RESULT" | jq -r '.path')
# 2. Use the Postiz URL in your post
postiz posts:create -c "Check out my video!" -s "2024-12-31T12:00:00Z" -m "$PATH" -i "tiktok-id"
```
**Why this is required:**
- **TikTok, Instagram, YouTube** only accept URLs from trusted domains
- **Security:** Platforms verify media sources to prevent abuse
- **Reliability:** Postiz ensures your media is always accessible
---
## Platform-Specific Features
### Reddit
```bash
# Get available flairs
postiz integrations:trigger reddit-id getFlairs -d '{"subreddit":"programming"}'
# Post with subreddit and flair
postiz posts:create \
-c "Content" \
-s "2024-12-31T12:00:00Z" \
--settings '{"subreddit":[{"value":{"subreddit":"programming","title":"My Post","type":"text","is_flair_required":true,"flair":{"id":"flair-123","name":"Discussion"}}}]}' \
-i "reddit-id"
```
### YouTube
```bash
# Get playlists
postiz integrations:trigger youtube-id getPlaylists
# Upload video FIRST (required!)
VIDEO=$(postiz upload video.mp4)
VIDEO_URL=$(echo "$VIDEO" | jq -r '.path')
# Post with uploaded video URL
postiz posts:create \
-c "Video description" \
-s "2024-12-31T12:00:00Z" \
--settings '{"title":"Video Title","type":"public","tags":[{"value":"tech","label":"Tech"}],"playlistId":"playlist-id"}' \
-m "$VIDEO_URL" \
-i "youtube-id"
```
### TikTok
```bash
# Upload video FIRST (TikTok only accepts verified URLs!)
VIDEO=$(postiz upload video.mp4)
VIDEO_URL=$(echo "$VIDEO" | jq -r '.path')
# Post with uploaded video URL
postiz posts:create \
-c "Video caption #fyp" \
-s "2024-12-31T12:00:00Z" \
--settings '{"privacy":"PUBLIC_TO_EVERYONE","duet":true,"stitch":true}' \
-m "$VIDEO_URL" \
-i "tiktok-id"
```
### LinkedIn
```bash
# Get companies you can post to
postiz integrations:trigger linkedin-id getCompanies
# Post as company
postiz posts:create \
-c "Company announcement" \
-s "2024-12-31T12:00:00Z" \
--settings '{"companyId":"company-123"}' \
-i "linkedin-id"
```
### X (Twitter)
```bash
# Create thread
postiz posts:create \
-c "Thread 1/3 🧵" \
-c "Thread 2/3" \
-c "Thread 3/3" \
-s "2024-12-31T12:00:00Z" \
-d 2000 \
-i "twitter-id"
# With reply settings
postiz posts:create \
-c "Tweet content" \
-s "2024-12-31T12:00:00Z" \
--settings '{"who_can_reply_post":"everyone"}' \
-i "twitter-id"
```
### Instagram
```bash
# Upload image FIRST (Instagram requires verified URLs!)
IMAGE=$(postiz upload image.jpg)
IMAGE_URL=$(echo "$IMAGE" | jq -r '.path')
# Regular post
postiz posts:create \
-c "Caption #hashtag" \
-s "2024-12-31T12:00:00Z" \
--settings '{"post_type":"post"}' \
-m "$IMAGE_URL" \
-i "instagram-id"
# Story (upload first)
STORY=$(postiz upload story.jpg)
STORY_URL=$(echo "$STORY" | jq -r '.path')
postiz posts:create \
-c "" \
-s "2024-12-31T12:00:00Z" \
--settings '{"post_type":"story"}' \
-m "$STORY_URL" \
-i "instagram-id"
```
**See [PROVIDER_SETTINGS.md](./PROVIDER_SETTINGS.md) for all 28+ platforms.**
---
## Features for AI Agents
### Discovery Workflow
The CLI enables dynamic discovery of integration capabilities:
1. **List integrations** - Get available social media accounts
2. **Get settings** - Retrieve character limits, required fields, and available tools
3. **Trigger tools** - Fetch dynamic data (flairs, playlists, boards, etc.)
4. **Create posts** - Use discovered data in posts
This allows AI agents to adapt to different platforms without hardcoded knowledge.
### JSON Mode
For complex posts with multiple platforms and settings:
```bash
postiz posts:create --json complex-post.json
```
JSON structure:
```json
{
"integrations": ["twitter-123", "linkedin-456"],
"posts": [
{
"provider": "twitter",
"post": [
{
"content": "Tweet version",
"image": ["twitter-image.jpg"]
}
]
},
{
"provider": "linkedin",
"post": [
{
"content": "LinkedIn version with more context...",
"image": ["linkedin-image.jpg"]
}
],
"settings": {
"__type": "linkedin",
"companyId": "company-123"
}
}
]
}
```
### All Output is JSON
Every command outputs JSON for easy parsing:
```bash
INTEGRATIONS=$(postiz integrations:list | jq -r '.')
REDDIT_ID=$(echo "$INTEGRATIONS" | jq -r '.[] | select(.identifier=="reddit") | .id')
```
### Threading Support
Comments are automatically converted to threads/replies based on platform:
- **Twitter/X**: Thread of tweets
- **Reddit**: Comment replies
- **LinkedIn**: Comment on post
- **Instagram**: First comment
```bash
postiz posts:create \
-c "Main post" \
-c "Comment 1" \
-c "Comment 2" \
-i "integration-id"
```
---
## Common Workflows
### Reddit Post with Flair
```bash
#!/bin/bash
REDDIT_ID=$(postiz integrations:list | jq -r '.[] | select(.identifier=="reddit") | .id')
FLAIRS=$(postiz integrations:trigger "$REDDIT_ID" getFlairs -d '{"subreddit":"programming"}')
FLAIR_ID=$(echo "$FLAIRS" | jq -r '.output[0].id')
postiz posts:create \
-c "My post content" \
-s "2024-12-31T12:00:00Z" \
--settings "{\"subreddit\":[{\"value\":{\"subreddit\":\"programming\",\"title\":\"Post Title\",\"type\":\"text\",\"is_flair_required\":true,\"flair\":{\"id\":\"$FLAIR_ID\",\"name\":\"Discussion\"}}}]}" \
-i "$REDDIT_ID"
```
### YouTube Video Upload
```bash
#!/bin/bash
VIDEO=$(postiz upload video.mp4)
VIDEO_PATH=$(echo "$VIDEO" | jq -r '.path')
postiz posts:create \
-c "Video description..." \
-s "2024-12-31T12:00:00Z" \
--settings '{"title":"My Video","type":"public","tags":[{"value":"tech","label":"Tech"}]}' \
-m "$VIDEO_PATH" \
-i "youtube-id"
```
### Multi-Platform Campaign
```bash
#!/bin/bash
postiz posts:create \
-c "Same content everywhere" \
-s "2024-12-31T12:00:00Z" \
-m "image.jpg" \
-i "twitter-id,linkedin-id,facebook-id"
```
### Batch Scheduling
```bash
#!/bin/bash
DATES=("2024-02-14T09:00:00Z" "2024-02-15T09:00:00Z" "2024-02-16T09:00:00Z")
CONTENT=("Monday motivation 💪" "Tuesday tips 💡" "Wednesday wisdom 🧠")
for i in "${!DATES[@]}"; do
postiz posts:create \
-c "${CONTENT[$i]}" \
-s "${DATES[$i]}" \
-i "twitter-id"
done
```
---
## Documentation
**For AI Agents:**
- **[SKILL.md](./SKILL.md)** - Complete skill reference with patterns and examples
**Deep-Dive Guides:**
- **[HOW_TO_RUN.md](./HOW_TO_RUN.md)** - Installation and setup methods
- **[COMMAND_LINE_GUIDE.md](./COMMAND_LINE_GUIDE.md)** - Complete command syntax reference
- **[PROVIDER_SETTINGS.md](./PROVIDER_SETTINGS.md)** - All platform settings schemas
- **[INTEGRATION_TOOLS_WORKFLOW.md](./INTEGRATION_TOOLS_WORKFLOW.md)** - Tools workflow guide
- **[INTEGRATION_SETTINGS_DISCOVERY.md](./INTEGRATION_SETTINGS_DISCOVERY.md)** - Settings discovery
- **[SUPPORTED_FILE_TYPES.md](./SUPPORTED_FILE_TYPES.md)** - Media format reference
- **[PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md)** - Code architecture
- **[PUBLISHING.md](./PUBLISHING.md)** - npm publishing guide
**Examples:**
- **[examples/EXAMPLES.md](./examples/EXAMPLES.md)** - Comprehensive examples
- **[examples/](./examples/)** - Ready-to-use scripts and JSON files
---
## API Endpoints
The CLI interacts with these Postiz API endpoints:
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/public/v1/posts` | POST | Create a post |
| `/public/v1/posts` | GET | List posts |
| `/public/v1/posts/:id` | DELETE | Delete a post |
| `/public/v1/integrations` | GET | List integrations |
| `/public/v1/integration-settings/:id` | GET | Get integration settings |
| `/public/v1/integration-trigger/:id` | POST | Trigger integration tool |
| `/public/v1/upload` | POST | Upload media |
---
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `POSTIZ_API_KEY` | ✅ Yes | - | Your Postiz API key |
| `POSTIZ_API_URL` | No | `https://api.postiz.com` | Custom API endpoint |
---
## Error Handling
The CLI provides clear error messages with exit codes:
- **Exit code 0**: Success
- **Exit code 1**: Error occurred
**Common errors:**
| Error | Solution |
|-------|----------|
| `POSTIZ_API_KEY is not set` | Set environment variable: `export POSTIZ_API_KEY=key` |
| `Integration not found` | Run `integrations:list` to get valid IDs |
| `startDate/endDate required` | Use ISO 8601 format: `"2024-12-31T12:00:00Z"` |
| `Invalid settings` | Check `integrations:settings` for required fields |
| `Tool not found` | Check available tools in `integrations:settings` output |
| `Upload failed` | Verify file exists and format is supported |
---
## Development
### Project Structure
```
apps/cli/
├── src/
│ ├── index.ts # CLI entry point with yargs
│ ├── api.ts # PostizAPI client class
│ ├── config.ts # Environment configuration
│ └── commands/
│ ├── posts.ts # Post management commands
│ ├── integrations.ts # Integration commands
│ └── upload.ts # Media upload command
├── examples/ # Example scripts and JSON files
├── package.json
├── tsconfig.json
├── tsup.config.ts # Build configuration
├── README.md # This file
└── SKILL.md # AI agent reference
```
### Scripts
```bash
pnpm run dev # Watch mode for development
pnpm run build # Build the CLI
pnpm run start # Run the built CLI
```
### Building
The CLI uses `tsup` for bundling:
```bash
pnpm run build
```
Output in `dist/`:
- `index.js` - Bundled executable with shebang
- `index.js.map` - Source map
---
## Quick Reference
```bash
# Environment setup
export POSTIZ_API_KEY=your_key
# Discovery
postiz integrations:list # List integrations
postiz integrations:settings <id> # Get settings
postiz integrations:trigger <id> <method> -d '{}' # Fetch data
# Posting (date is required)
postiz posts:create -c "text" -s "2024-12-31T12:00:00Z" -i "id" # Simple
postiz posts:create -c "text" -s "2024-12-31T12:00:00Z" -t draft -i "id" # Draft
postiz posts:create -c "text" -m "img.jpg" -s "2024-12-31T12:00:00Z" -i "id" # With media
postiz posts:create -c "main" -c "comment" -s "2024-12-31T12:00:00Z" -i "id" # With comment
postiz posts:create -c "text" -s "2024-12-31T12:00:00Z" --settings '{}' -i "id" # Platform-specific
postiz posts:create --json file.json # Complex
# Management
postiz posts:list # List posts
postiz posts:delete <id> # Delete post
postiz upload <file> # Upload media
# Help
postiz --help # Show help
postiz posts:create --help # Command help
```
---
## Contributing
This CLI is part of the [Postiz monorepo](https://github.com/gitroomhq/postiz-app).
To contribute:
1. Fork the repository
2. Create a feature branch
3. Make your changes in `apps/cli/`
4. Run tests: `pnpm run build`
5. Submit a pull request
---
## License
AGPL-3.0
---
## Links
- **Website:** [postiz.com](https://postiz.com)
- **API Docs:** [postiz.com/api-docs](https://postiz.com/api-docs)
- **GitHub:** [gitroomhq/postiz-app](https://github.com/gitroomhq/postiz-app)
- **Issues:** [Report bugs](https://github.com/gitroomhq/postiz-app/issues)
---
## Supported Platforms
28+ platforms including:
| Platform | Integration Tools | Settings |
|----------|------------------|----------|
| Twitter/X | getLists, getCommunities | who_can_reply_post |
| LinkedIn | getCompanies | companyId, carousel |
| Reddit | getFlairs, searchSubreddits | subreddit, title, flair |
| YouTube | getPlaylists, getCategories | title, type, tags, playlistId |
| TikTok | - | privacy, duet, stitch |
| Instagram | - | post_type (post/story) |
| Facebook | getPages | - |
| Pinterest | getBoards, getBoardSections | - |
| Discord | getChannels | - |
| Slack | getChannels | - |
| And 18+ more... | | |
**See [PROVIDER_SETTINGS.md](./PROVIDER_SETTINGS.md) for complete documentation.**

View file

@ -1,607 +0,0 @@
| Property | Value |
|----------|-------|
| **name** | postiz |
| **description** | Social media automation CLI for scheduling posts across 28+ platforms |
| **allowed-tools** | Bash(postiz:*) |
---
## Core Workflow
The fundamental pattern for using Postiz CLI:
1. **Discover** - List integrations and get their settings
2. **Fetch** - Use integration tools to retrieve dynamic data (flairs, playlists, companies)
3. **Prepare** - Upload media files if needed
4. **Post** - Create posts with content, media, and platform-specific settings
```bash
# 1. Discover
postiz integrations:list
postiz integrations:settings <integration-id>
# 2. Fetch (if needed)
postiz integrations:trigger <integration-id> <method> -d '{"key":"value"}'
# 3. Prepare
postiz upload image.jpg
# 4. Post
postiz posts:create -c "Content" -m "image.jpg" -i "<integration-id>"
```
---
## Essential Commands
### Setup
```bash
# Required environment variable
export POSTIZ_API_KEY=your_api_key_here
# Optional custom API URL
export POSTIZ_API_URL=https://custom-api-url.com
```
### Integration Discovery
```bash
# List all connected integrations
postiz integrations:list
# Get settings schema for specific integration
postiz integrations:settings <integration-id>
# Trigger integration tool to fetch dynamic data
postiz integrations:trigger <integration-id> <method-name>
postiz integrations:trigger <integration-id> <method-name> -d '{"param":"value"}'
```
### Creating Posts
```bash
# Simple post (date is REQUIRED)
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -i "integration-id"
# Draft post
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -t draft -i "integration-id"
# Post with media
postiz posts:create -c "Content" -m "img1.jpg,img2.jpg" -s "2024-12-31T12:00:00Z" -i "integration-id"
# Post with comments (each with own media)
postiz posts:create \
-c "Main post" -m "main.jpg" \
-c "First comment" -m "comment1.jpg" \
-c "Second comment" -m "comment2.jpg,comment3.jpg" \
-s "2024-12-31T12:00:00Z" \
-i "integration-id"
# Multi-platform post
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -i "twitter-id,linkedin-id,facebook-id"
# Platform-specific settings
postiz posts:create \
-c "Content" \
-s "2024-12-31T12:00:00Z" \
--settings '{"subreddit":[{"value":{"subreddit":"programming","title":"My Post","type":"text"}}]}' \
-i "reddit-id"
# Complex post from JSON file
postiz posts:create --json post.json
```
### Managing Posts
```bash
# List posts (defaults to last 30 days to next 30 days)
postiz posts:list
# List posts in date range
postiz posts:list --startDate "2024-01-01T00:00:00Z" --endDate "2024-12-31T23:59:59Z"
# Delete post
postiz posts:delete <post-id>
```
### Media Upload
**⚠️ IMPORTANT:** Always upload files to Postiz before using them in posts. Many platforms (TikTok, Instagram, YouTube) **require verified URLs** and will reject external links.
```bash
# Upload file and get URL
postiz upload image.jpg
# Supports: images (PNG, JPG, GIF, WEBP, SVG), videos (MP4, MOV, AVI, MKV, WEBM),
# audio (MP3, WAV, OGG, AAC), documents (PDF, DOC, DOCX)
# Workflow: Upload → Extract URL → Use in post
VIDEO=$(postiz upload video.mp4)
VIDEO_PATH=$(echo "$VIDEO" | jq -r '.path')
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -m "$VIDEO_PATH" -i "tiktok-id"
```
---
## Common Patterns
### Pattern 1: Discover & Use Integration Tools
**Reddit - Get flairs for a subreddit:**
```bash
# Get Reddit integration ID
REDDIT_ID=$(postiz integrations:list | jq -r '.[] | select(.identifier=="reddit") | .id')
# Fetch available flairs
FLAIRS=$(postiz integrations:trigger "$REDDIT_ID" getFlairs -d '{"subreddit":"programming"}')
FLAIR_ID=$(echo "$FLAIRS" | jq -r '.output[0].id')
# Use in post
postiz posts:create \
-c "My post content" \
-s "2024-12-31T12:00:00Z" \
--settings "{\"subreddit\":[{\"value\":{\"subreddit\":\"programming\",\"title\":\"Post Title\",\"type\":\"text\",\"is_flair_required\":true,\"flair\":{\"id\":\"$FLAIR_ID\",\"name\":\"Discussion\"}}}]}" \
-i "$REDDIT_ID"
```
**YouTube - Get playlists:**
```bash
YOUTUBE_ID=$(postiz integrations:list | jq -r '.[] | select(.identifier=="youtube") | .id')
PLAYLISTS=$(postiz integrations:trigger "$YOUTUBE_ID" getPlaylists)
PLAYLIST_ID=$(echo "$PLAYLISTS" | jq -r '.output[0].id')
postiz posts:create \
-c "Video description" \
-s "2024-12-31T12:00:00Z" \
--settings "{\"title\":\"My Video\",\"type\":\"public\",\"playlistId\":\"$PLAYLIST_ID\"}" \
-m "video.mp4" \
-i "$YOUTUBE_ID"
```
**LinkedIn - Post as company:**
```bash
LINKEDIN_ID=$(postiz integrations:list | jq -r '.[] | select(.identifier=="linkedin") | .id')
COMPANIES=$(postiz integrations:trigger "$LINKEDIN_ID" getCompanies)
COMPANY_ID=$(echo "$COMPANIES" | jq -r '.output[0].id')
postiz posts:create \
-c "Company announcement" \
-s "2024-12-31T12:00:00Z" \
--settings "{\"companyId\":\"$COMPANY_ID\"}" \
-i "$LINKEDIN_ID"
```
### Pattern 2: Upload Media Before Posting
```bash
# Upload multiple files
VIDEO_RESULT=$(postiz upload video.mp4)
VIDEO_PATH=$(echo "$VIDEO_RESULT" | jq -r '.path')
THUMB_RESULT=$(postiz upload thumbnail.jpg)
THUMB_PATH=$(echo "$THUMB_RESULT" | jq -r '.path')
# Use in post
postiz posts:create \
-c "Check out my video!" \
-s "2024-12-31T12:00:00Z" \
-m "$VIDEO_PATH" \
-i "tiktok-id"
```
### Pattern 3: Twitter Thread
```bash
postiz posts:create \
-c "🧵 Thread starter (1/4)" -m "intro.jpg" \
-c "Point one (2/4)" -m "point1.jpg" \
-c "Point two (3/4)" -m "point2.jpg" \
-c "Conclusion (4/4)" -m "outro.jpg" \
-s "2024-12-31T12:00:00Z" \
-d 2000 \
-i "twitter-id"
```
### Pattern 4: Multi-Platform Campaign
```bash
# Create JSON file with platform-specific content
cat > campaign.json << 'EOF'
{
"integrations": ["twitter-123", "linkedin-456", "facebook-789"],
"posts": [
{
"provider": "twitter",
"post": [
{
"content": "Short tweet version #tech",
"image": ["twitter-image.jpg"]
}
]
},
{
"provider": "linkedin",
"post": [
{
"content": "Professional LinkedIn version with more context...",
"image": ["linkedin-image.jpg"]
}
]
}
]
}
EOF
postiz posts:create --json campaign.json
```
### Pattern 5: Validate Settings Before Posting
```javascript
const { execSync } = require('child_process');
function validateAndPost(content, integrationId, settings) {
// Get integration settings
const settingsResult = execSync(
`postiz integrations:settings ${integrationId}`,
{ encoding: 'utf-8' }
);
const schema = JSON.parse(settingsResult);
// Check character limit
if (content.length > schema.output.maxLength) {
console.warn(`Content exceeds ${schema.output.maxLength} chars, truncating...`);
content = content.substring(0, schema.output.maxLength - 3) + '...';
}
// Create post
const result = execSync(
`postiz posts:create -c "${content}" -s "2024-12-31T12:00:00Z" --settings '${JSON.stringify(settings)}' -i "${integrationId}"`,
{ encoding: 'utf-8' }
);
return JSON.parse(result);
}
```
### Pattern 6: Batch Scheduling
```bash
#!/bin/bash
# Schedule posts for the week
DATES=(
"2024-02-14T09:00:00Z"
"2024-02-15T09:00:00Z"
"2024-02-16T09:00:00Z"
)
CONTENT=(
"Monday motivation 💪"
"Tuesday tips 💡"
"Wednesday wisdom 🧠"
)
for i in "${!DATES[@]}"; do
postiz posts:create \
-c "${CONTENT[$i]}" \
-s "${DATES[$i]}" \
-i "twitter-id" \
-m "post-${i}.jpg"
echo "Scheduled: ${CONTENT[$i]} for ${DATES[$i]}"
done
```
### Pattern 7: Error Handling & Retry
```javascript
const { execSync } = require('child_process');
async function postWithRetry(content, integrationId, date, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = execSync(
`postiz posts:create -c "${content}" -s "${date}" -i "${integrationId}"`,
{ encoding: 'utf-8', stdio: 'pipe' }
);
console.log('✅ Post created successfully');
return JSON.parse(result);
} catch (error) {
console.error(`❌ Attempt ${attempt} failed: ${error.message}`);
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
console.log(`⏳ Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw new Error(`Failed after ${maxRetries} attempts`);
}
}
}
}
```
---
## Technical Concepts
### Integration Tools Workflow
Many integrations require dynamic data (IDs, tags, playlists) that can't be hardcoded. The tools workflow enables discovery and usage:
1. **Check available tools** - `integrations:settings` returns a `tools` array
2. **Review tool schema** - Each tool has `methodName`, `description`, and `dataSchema`
3. **Trigger tool** - Call `integrations:trigger` with required parameters
4. **Use output** - Tool returns data to use in post settings
**Example tools by platform:**
- **Reddit**: `getFlairs`, `searchSubreddits`, `getSubreddits`
- **YouTube**: `getPlaylists`, `getCategories`, `getChannels`
- **LinkedIn**: `getCompanies`, `getOrganizations`
- **Twitter/X**: `getListsowned`, `getCommunities`
- **Pinterest**: `getBoards`, `getBoardSections`
### Provider Settings Structure
Platform-specific settings use a discriminator pattern with `__type` field:
```json
{
"posts": [
{
"provider": "reddit",
"post": [{ "content": "...", "image": [...] }],
"settings": {
"__type": "reddit",
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "Post Title",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}
}
]
}
```
Pass settings directly:
```bash
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" --settings '{"subreddit":[...]}' -i "reddit-id"
# Backend automatically adds "__type" based on integration ID
```
### Comments and Threading
Posts can have comments (threads on Twitter/X, replies elsewhere). Each comment can have its own media:
```bash
# Using multiple -c and -m flags
postiz posts:create \
-c "Main post" -m "image1.jpg,image2.jpg" \
-c "Comment 1" -m "comment-img.jpg" \
-c "Comment 2" -m "another.jpg,more.jpg" \
-s "2024-12-31T12:00:00Z" \
-d 5000 \ # Delay between comments in ms
-i "integration-id"
```
Internally creates:
```json
{
"posts": [{
"value": [
{ "content": "Main post", "image": ["image1.jpg", "image2.jpg"] },
{ "content": "Comment 1", "image": ["comment-img.jpg"], "delay": 5000 },
{ "content": "Comment 2", "image": ["another.jpg", "more.jpg"], "delay": 5000 }
]
}]
}
```
### Date Handling
All dates use ISO 8601 format:
- Schedule posts: `-s "2024-12-31T12:00:00Z"`
- List posts: `--startDate "2024-01-01T00:00:00Z" --endDate "2024-12-31T23:59:59Z"`
- Defaults: `posts:list` uses 30 days ago to 30 days from now
### Media Upload Response
Upload returns JSON with path and metadata:
```json
{
"path": "https://cdn.postiz.com/uploads/abc123.jpg",
"size": 123456,
"type": "image/jpeg"
}
```
Extract path for use in posts:
```bash
RESULT=$(postiz upload image.jpg)
PATH=$(echo "$RESULT" | jq -r '.path')
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -m "$PATH" -i "integration-id"
```
### JSON Mode vs CLI Flags
**CLI flags** - Quick posts:
```bash
postiz posts:create -c "Content" -m "img.jpg" -i "twitter-id"
```
**JSON mode** - Complex posts with multiple platforms and settings:
```bash
postiz posts:create --json post.json
```
JSON mode supports:
- Multiple platforms with different content per platform
- Complex provider-specific settings
- Scheduled posts
- Posts with many comments
- Custom delay between comments
---
## Platform-Specific Examples
### Reddit
```bash
postiz posts:create \
-c "Post content" \
-s "2024-12-31T12:00:00Z" \
--settings '{"subreddit":[{"value":{"subreddit":"programming","title":"My Title","type":"text","url":"","is_flair_required":false}}]}' \
-i "reddit-id"
```
### YouTube
```bash
# Upload video first (required!)
VIDEO=$(postiz upload video.mp4)
VIDEO_URL=$(echo "$VIDEO" | jq -r '.path')
postiz posts:create \
-c "Video description" \
-s "2024-12-31T12:00:00Z" \
--settings '{"title":"Video Title","type":"public","tags":[{"value":"tech","label":"Tech"}]}' \
-m "$VIDEO_URL" \
-i "youtube-id"
```
### TikTok
```bash
# Upload video first (TikTok only accepts verified URLs!)
VIDEO=$(postiz upload video.mp4)
VIDEO_URL=$(echo "$VIDEO" | jq -r '.path')
postiz posts:create \
-c "Video caption #fyp" \
-s "2024-12-31T12:00:00Z" \
--settings '{"privacy":"PUBLIC_TO_EVERYONE","duet":true,"stitch":true}' \
-m "$VIDEO_URL" \
-i "tiktok-id"
```
### X (Twitter)
```bash
postiz posts:create \
-c "Tweet content" \
-s "2024-12-31T12:00:00Z" \
--settings '{"who_can_reply_post":"everyone"}' \
-i "twitter-id"
```
### LinkedIn
```bash
# Personal post
postiz posts:create -c "Content" -s "2024-12-31T12:00:00Z" -i "linkedin-id"
# Company post
postiz posts:create \
-c "Content" \
-s "2024-12-31T12:00:00Z" \
--settings '{"companyId":"company-123"}' \
-i "linkedin-id"
```
### Instagram
```bash
# Upload image first (Instagram requires verified URLs!)
IMAGE=$(postiz upload image.jpg)
IMAGE_URL=$(echo "$IMAGE" | jq -r '.path')
# Regular post
postiz posts:create \
-c "Caption #hashtag" \
-s "2024-12-31T12:00:00Z" \
--settings '{"post_type":"post"}' \
-m "$IMAGE_URL" \
-i "instagram-id"
# Story
STORY=$(postiz upload story.jpg)
STORY_URL=$(echo "$STORY" | jq -r '.path')
postiz posts:create \
-c "" \
-s "2024-12-31T12:00:00Z" \
--settings '{"post_type":"story"}' \
-m "$STORY_URL" \
-i "instagram-id"
```
---
## Supporting Resources
**Deep-dive documentation:**
- [HOW_TO_RUN.md](./HOW_TO_RUN.md) - Installation and setup methods
- [COMMAND_LINE_GUIDE.md](./COMMAND_LINE_GUIDE.md) - Complete command syntax reference
- [PROVIDER_SETTINGS.md](./PROVIDER_SETTINGS.md) - All 28+ platform settings schemas
- [INTEGRATION_TOOLS_WORKFLOW.md](./INTEGRATION_TOOLS_WORKFLOW.md) - Complete tools workflow guide
- [INTEGRATION_SETTINGS_DISCOVERY.md](./INTEGRATION_SETTINGS_DISCOVERY.md) - Settings discovery workflow
- [SUPPORTED_FILE_TYPES.md](./SUPPORTED_FILE_TYPES.md) - All supported media formats
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - Code architecture
- [PUBLISHING.md](./PUBLISHING.md) - npm publishing guide
**Ready-to-use examples:**
- [examples/EXAMPLES.md](./examples/EXAMPLES.md) - Comprehensive examples
- [examples/basic-usage.sh](./examples/basic-usage.sh) - Shell script basics
- [examples/ai-agent-example.js](./examples/ai-agent-example.js) - Node.js agent
- [examples/post-with-comments.json](./examples/post-with-comments.json) - Threading example
- [examples/multi-platform-with-settings.json](./examples/multi-platform-with-settings.json) - Campaign example
- [examples/youtube-video.json](./examples/youtube-video.json) - YouTube with tags
- [examples/reddit-post.json](./examples/reddit-post.json) - Reddit with subreddit
- [examples/tiktok-video.json](./examples/tiktok-video.json) - TikTok with privacy
---
## Common Gotchas
1. **API Key not set** - Always `export POSTIZ_API_KEY=key` before using CLI
2. **Invalid integration ID** - Run `integrations:list` to get current IDs
3. **Settings schema mismatch** - Check `integrations:settings` for required fields
4. **Media MUST be uploaded to Postiz first** - ⚠️ **CRITICAL:** TikTok, Instagram, YouTube, and many platforms only accept verified URLs. Upload files via `postiz upload` first, then use the returned URL in `-m`. External URLs will be rejected!
5. **JSON escaping in shell** - Use single quotes for JSON: `--settings '{...}'`
6. **Date format** - Must be ISO 8601: `"2024-12-31T12:00:00Z"` and is REQUIRED
7. **Tool not found** - Check available tools in `integrations:settings` output
8. **Character limits** - Each platform has different limits, check `maxLength` in settings
9. **Required settings** - Some platforms require specific settings (Reddit needs title, YouTube needs title)
10. **Media MIME types** - CLI auto-detects from file extension, ensure correct extension
---
## Quick Reference
```bash
# Environment
export POSTIZ_API_KEY=key
# Discovery
postiz integrations:list # Get integration IDs
postiz integrations:settings <id> # Get settings schema
postiz integrations:trigger <id> <method> -d '{}' # Fetch dynamic data
# Posting (date is REQUIRED)
postiz posts:create -c "text" -s "2024-12-31T12:00:00Z" -i "id" # Simple
postiz posts:create -c "text" -s "2024-12-31T12:00:00Z" -t draft -i "id" # Draft
postiz posts:create -c "text" -m "img.jpg" -s "2024-12-31T12:00:00Z" -i "id" # With media
postiz posts:create -c "main" -c "comment" -s "2024-12-31T12:00:00Z" -i "id" # With comment
postiz posts:create -c "text" -s "2024-12-31T12:00:00Z" --settings '{}' -i "id" # Platform-specific
postiz posts:create --json file.json # Complex
# Management
postiz posts:list # List posts
postiz posts:delete <id> # Delete post
postiz upload <file> # Upload media
# Help
postiz --help # Show help
postiz posts:create --help # Command help
```

View file

@ -1,281 +0,0 @@
# Postiz CLI - Creation Summary
## ✅ What Was Created
A complete, production-ready CLI package for the Postiz API has been successfully created at `apps/cli/`.
### Package Details
- **Package Name:** `postiz`
- **Version:** 1.0.0
- **Executable:** `postiz` command
- **Lines of Code:** 359 lines
- **Build Size:** ~491KB (compressed)
- **License:** AGPL-3.0
## 📦 Package Structure
```
apps/cli/
├── src/ # Source code (359 lines)
│ ├── index.ts # CLI entry point with yargs
│ ├── api.ts # Postiz API client
│ ├── config.ts # Environment configuration
│ └── commands/
│ ├── posts.ts # Post management
│ ├── integrations.ts # Integration listing
│ └── upload.ts # Media upload
├── examples/ # Usage examples
│ ├── basic-usage.sh # Bash example
│ └── ai-agent-example.js # AI agent example
├── Documentation (5 files)
│ ├── README.md # Main documentation
│ ├── SKILL.md # AI agent guide
│ ├── QUICK_START.md # Quick start guide
│ ├── CHANGELOG.md # Version history
│ └── PROJECT_STRUCTURE.md # Architecture docs
└── Configuration
├── package.json # Package config
├── tsconfig.json # TypeScript config
├── tsup.config.ts # Build config
├── .gitignore # Git ignore
└── .npmignore # npm ignore
```
## 🚀 Features Implemented
### Commands
1. **posts:create** - Create social media posts
- ✅ Content input
- ✅ Integration selection
- ✅ Scheduled posting
- ✅ Image attachment
2. **posts:list** - List all posts
- ✅ Pagination support
- ✅ Search functionality
- ✅ Filtering options
3. **posts:delete** - Delete posts by ID
- ✅ ID-based deletion
- ✅ Confirmation messages
4. **integrations:list** - Show connected accounts
- ✅ List all integrations
- ✅ Show provider info
5. **upload** - Upload media files
- ✅ Image upload support
- ✅ Multiple formats (PNG, JPG, GIF)
### Technical Features
- ✅ Environment variable configuration (POSTIZ_API_KEY)
- ✅ Custom API URL support (POSTIZ_API_URL)
- ✅ Comprehensive error handling
- ✅ User-friendly error messages with emojis
- ✅ JSON output for programmatic parsing
- ✅ Executable shebang for direct execution
- ✅ TypeScript with proper types
- ✅ Source maps for debugging
- ✅ Build optimization with tsup
## 📚 Documentation Created
1. **README.md** (Primary documentation)
- Installation instructions
- Usage examples
- API reference
- Development guide
2. **SKILL.md** (AI Agent Guide)
- Comprehensive patterns for AI agents
- Usage examples
- Workflow suggestions
- Best practices
- Error handling
3. **QUICK_START.md**
- Fast onboarding
- Common workflows
- Troubleshooting
- Tips & tricks
4. **CHANGELOG.md**
- Version 1.0.0 release notes
- Feature list
5. **PROJECT_STRUCTURE.md**
- Architecture overview
- File descriptions
- Build process
- Integration points
## 🔧 Build System Integration
### Root package.json Scripts Added
```json
{
"build:cli": "rm -rf apps/cli/dist && pnpm --filter ./apps/cli run build",
"publish-cli": "pnpm run --filter ./apps/cli publish"
}
```
### CLI Package Scripts
```json
{
"dev": "tsup --watch",
"build": "tsup",
"start": "node ./dist/index.js",
"publish": "tsup && pnpm publish --access public"
}
```
## 🎯 Usage Examples
### Basic Usage
```bash
# Set API key
export POSTIZ_API_KEY=your_api_key
# Create a post
postiz posts:create -c "Hello World!" -i "twitter-123"
# List posts
postiz posts:list
# Upload media
postiz upload ./image.png
```
### AI Agent Usage
```javascript
const { execSync } = require('child_process');
function postToSocial(content) {
return execSync(`postiz posts:create -c "${content}"`, {
env: { ...process.env, POSTIZ_API_KEY: 'your_key' }
});
}
```
## ✨ Example Files
1. **basic-usage.sh**
- Shell script demonstration
- Complete workflow example
- Error handling
2. **ai-agent-example.js**
- Node.js agent implementation
- Batch post creation
- JSON parsing
## 🧪 Testing
### Manual Testing Completed
```bash
✅ Build successful (173ms)
✅ Help command works
✅ Version command works (1.0.0)
✅ Error handling works (API key validation)
✅ All commands have help text
✅ Examples are valid
```
### Test Results
```
✅ pnpm run build:cli - SUCCESS
✅ postiz --help - SUCCESS
✅ postiz --version - SUCCESS
✅ postiz posts:create --help - SUCCESS
✅ Error without API key - WORKS AS EXPECTED
```
## 📋 Checklist
- ✅ CLI package created in apps/cli
- ✅ Package name is "postiz"
- ✅ Uses POSTIZ_API_KEY environment variable
- ✅ Integrates with Postiz public API
- ✅ Built for AI agent usage
- ✅ SKILL.md created with comprehensive guide
- ✅ README.md with full documentation
- ✅ Build system configured
- ✅ TypeScript compilation working
- ✅ Executable binary generated
- ✅ Examples provided
- ✅ Error handling implemented
- ✅ Help documentation complete
## 🚦 Next Steps
### To Use Locally
```bash
# Build the CLI
pnpm run build:cli
# Test it
node apps/cli/dist/index.js --help
# Link globally (optional)
cd apps/cli
pnpm link --global
# Use anywhere
postiz --help
```
### To Publish to npm
```bash
# From monorepo root
pnpm run publish-cli
# Or from apps/cli
cd apps/cli
pnpm run publish
```
### To Use in AI Agents
1. Install: `npm install -g postiz`
2. Set API key: `export POSTIZ_API_KEY=your_key`
3. Use commands programmatically
4. Parse JSON output
5. See SKILL.md for patterns
## 📊 Statistics
- **Total Files Created:** 18
- **Source Code Files:** 6
- **Documentation Files:** 5
- **Example Files:** 2
- **Config Files:** 5
- **Total Lines of Code:** 359
- **Build Time:** ~170ms
- **Output Size:** 491KB
## 🎉 Summary
A complete, production-ready CLI tool for Postiz has been created with:
- ✅ All requested features implemented
- ✅ Comprehensive documentation for users and AI agents
- ✅ Working examples
- ✅ Proper build system
- ✅ Ready for npm publishing
- ✅ Integrated into monorepo
The CLI is ready to use and can be published to npm whenever you're ready!

View file

@ -1,305 +0,0 @@
# Supported File Types for Upload
The Postiz CLI now correctly detects and uploads various media types.
## How It Works
The CLI automatically detects the MIME type based on the file extension:
```bash
postiz upload video.mp4
# ✅ Detected as: video/mp4
postiz upload image.png
# ✅ Detected as: image/png
postiz upload audio.mp3
# ✅ Detected as: audio/mpeg
```
## Supported File Types
### Images
| Extension | MIME Type | Supported |
|-----------|-----------|-----------|
| `.png` | `image/png` | ✅ Yes |
| `.jpg`, `.jpeg` | `image/jpeg` | ✅ Yes |
| `.gif` | `image/gif` | ✅ Yes |
| `.webp` | `image/webp` | ✅ Yes |
| `.svg` | `image/svg+xml` | ✅ Yes |
| `.bmp` | `image/bmp` | ✅ Yes |
| `.ico` | `image/x-icon` | ✅ Yes |
**Examples:**
```bash
postiz upload photo.jpg
postiz upload logo.png
postiz upload animation.gif
postiz upload icon.svg
```
### Videos
| Extension | MIME Type | Supported |
|-----------|-----------|-----------|
| `.mp4` | `video/mp4` | ✅ Yes |
| `.mov` | `video/quicktime` | ✅ Yes |
| `.avi` | `video/x-msvideo` | ✅ Yes |
| `.mkv` | `video/x-matroska` | ✅ Yes |
| `.webm` | `video/webm` | ✅ Yes |
| `.flv` | `video/x-flv` | ✅ Yes |
| `.wmv` | `video/x-ms-wmv` | ✅ Yes |
| `.m4v` | `video/x-m4v` | ✅ Yes |
| `.mpeg`, `.mpg` | `video/mpeg` | ✅ Yes |
| `.3gp` | `video/3gpp` | ✅ Yes |
**Examples:**
```bash
postiz upload video.mp4
postiz upload clip.mov
postiz upload recording.webm
postiz upload movie.mkv
```
### Audio
| Extension | MIME Type | Supported |
|-----------|-----------|-----------|
| `.mp3` | `audio/mpeg` | ✅ Yes |
| `.wav` | `audio/wav` | ✅ Yes |
| `.ogg` | `audio/ogg` | ✅ Yes |
| `.aac` | `audio/aac` | ✅ Yes |
| `.flac` | `audio/flac` | ✅ Yes |
| `.m4a` | `audio/mp4` | ✅ Yes |
**Examples:**
```bash
postiz upload podcast.mp3
postiz upload song.wav
postiz upload audio.ogg
```
### Documents
| Extension | MIME Type | Supported |
|-----------|-----------|-----------|
| `.pdf` | `application/pdf` | ✅ Yes |
| `.doc` | `application/msword` | ✅ Yes |
| `.docx` | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | ✅ Yes |
**Examples:**
```bash
postiz upload document.pdf
postiz upload report.docx
```
### Other Files
For file types not listed above, the CLI uses:
- MIME type: `application/octet-stream`
- This is a generic binary file type
## Usage Examples
### Upload an Image
```bash
postiz upload ./images/photo.jpg
```
Response:
```json
{
"id": "upload-123",
"path": "https://cdn.postiz.com/uploads/photo.jpg",
"url": "https://cdn.postiz.com/uploads/photo.jpg"
}
```
### Upload a Video (MP4)
```bash
postiz upload ./videos/promo.mp4
```
Response:
```json
{
"id": "upload-456",
"path": "https://cdn.postiz.com/uploads/promo.mp4",
"url": "https://cdn.postiz.com/uploads/promo.mp4"
}
```
### Upload and Use in Post
```bash
# 1. Upload the file
RESULT=$(postiz upload video.mp4)
echo $RESULT
# 2. Extract the path (you'll need jq or similar)
PATH=$(echo $RESULT | jq -r '.path')
# 3. Use in a post
postiz posts:create \
-c "Check out my video!" \
-m "$PATH" \
-i "tiktok-123"
```
### Upload Multiple Files
```bash
# Upload images
postiz upload image1.jpg
postiz upload image2.png
postiz upload image3.gif
# Upload videos
postiz upload video1.mp4
postiz upload video2.mov
```
## What Changed (Fix)
### Before (❌ Bug)
```bash
postiz upload video.mp4
# ❌ Was detected as: image/jpeg (WRONG!)
```
The problem: The CLI defaulted to `image/jpeg` for any unknown file type.
### After (✅ Fixed)
```bash
postiz upload video.mp4
# ✅ Correctly detected as: video/mp4
postiz upload audio.mp3
# ✅ Correctly detected as: audio/mpeg
postiz upload document.pdf
# ✅ Correctly detected as: application/pdf
```
## Platform-Specific Notes
### TikTok
- Supports: MP4, MOV, WEBM
- Recommended: MP4
### YouTube
- Supports: MP4, MOV, AVI, WMV, FLV, 3GP, WEBM
- Recommended: MP4
### Instagram
- Images: JPG, PNG
- Videos: MP4, MOV
- Recommended: MP4 for videos, JPG for images
### Twitter/X
- Images: PNG, JPG, GIF, WEBP
- Videos: MP4, MOV
- Max video size: 512MB
### LinkedIn
- Images: PNG, JPG, GIF
- Videos: MP4, MOV, AVI
- Documents: PDF, DOC, DOCX, PPT
## Troubleshooting
### "Upload failed: Unsupported file type"
Some platforms may not accept certain file types. Check the platform's documentation.
**Solution:** Convert the file to a supported format:
```bash
# Convert video to MP4
ffmpeg -i video.avi video.mp4
# Then upload
postiz upload video.mp4
```
### File Size Limits
Different platforms have different file size limits:
- **Twitter/X**: Max 512MB for videos
- **Instagram**: Max 100MB for videos
- **TikTok**: Max 287.6MB for videos
- **YouTube**: Max 128GB (but 256GB for verified)
### "MIME type mismatch"
If you renamed a file with the wrong extension:
```bash
# ❌ Wrong: PNG file renamed to .jpg
mv image.png image.jpg
postiz upload image.jpg # Might fail
# ✅ Correct: Keep original extension
postiz upload image.png
```
## Testing File Upload
```bash
# Set API key
export POSTIZ_API_KEY=your_key
# Test image upload
postiz upload test-image.jpg
# Test video upload
postiz upload test-video.mp4
# Test audio upload
postiz upload test-audio.mp3
```
## Error Messages
### File Not Found
```
❌ ENOENT: no such file or directory
```
**Solution:** Check the file path is correct.
### No Permission
```
❌ EACCES: permission denied
```
**Solution:** Check file permissions:
```bash
chmod 644 your-file.mp4
```
### Invalid API Key
```
❌ Upload failed (401): Unauthorized
```
**Solution:** Set your API key:
```bash
export POSTIZ_API_KEY=your_key
```
## Summary
✅ **30+ file types supported**
✅ **Automatic MIME type detection**
✅ **Images, videos, audio, documents**
✅ **Correct handling of MP4, MOV, MP3, etc.**
✅ **No more defaulting to JPEG!**
**The upload bug is fixed!** 🎉

View file

@ -1,291 +0,0 @@
# Postiz CLI - Improved Syntax! 🎉
## What Changed
The CLI now supports a **much better** command-line syntax for creating posts with comments that have their own media.
## New Syntax: Multiple `-c` and `-m` Flags
Instead of using semicolon-separated strings (which break when you need semicolons in your content), you can now use multiple `-c` and `-m` flags:
```bash
postiz posts:create \
-c "main post content" -m "media1.png,media2.png" \
-c "first comment" -m "media3.png" \
-c "second comment; with semicolon!" -m "media4.png,media5.png" \
-i "twitter-123"
```
## The Problem We Solved
### ❌ Old Approach (Problematic)
```bash
postiz posts:create \
-c "Main post" \
--comments "Comment 1;Comment 2;Comment 3" \
-i "twitter-123"
```
**Issues:**
1. ❌ Can't use semicolons in comment text
2. ❌ Comments can't have their own media
3. ❌ Less intuitive syntax
4. ❌ Limited flexibility
### ✅ New Approach (Better!)
```bash
postiz posts:create \
-c "Main post" -m "main.jpg" \
-c "Comment 1; with semicolon!" -m "comment1.jpg" \
-c "Comment 2" -m "comment2.jpg" \
-c "Comment 3" \
-i "twitter-123"
```
**Benefits:**
1. ✅ Semicolons work fine in content
2. ✅ Each comment can have different media
3. ✅ More readable and intuitive
4. ✅ Fully flexible
## How It Works
### Pairing Logic
The CLI pairs `-c` and `-m` flags in order:
```bash
postiz posts:create \
-c "Content 1" -m "media-for-content-1.jpg" \ # Pair 1
-c "Content 2" -m "media-for-content-2.jpg" \ # Pair 2
-c "Content 3" -m "media-for-content-3.jpg" \ # Pair 3
-i "twitter-123"
```
- **1st `-c`** = Main post
- **2nd `-c`** = First comment (posted after delay)
- **3rd `-c`** = Second comment (posted after delay)
- Each `-m` is paired with the corresponding `-c` (in order)
### Media is Optional
```bash
postiz posts:create \
-c "Post with media" -m "image.jpg" \
-c "Comment without media" \
-c "Another comment" \
-i "twitter-123"
```
Result:
- Post with image
- Text-only comment
- Another text-only comment
### Multiple Media per Post/Comment
```bash
postiz posts:create \
-c "Main post" -m "img1.jpg,img2.jpg,img3.jpg" \
-c "Comment" -m "img4.jpg,img5.jpg" \
-i "twitter-123"
```
Result:
- Main post with 3 images
- Comment with 2 images
## Real Examples
### Example 1: Product Launch
```bash
postiz posts:create \
-c "🚀 Launching ProductX today!" \
-m "hero.jpg,features.jpg" \
-c "⭐ Key features you'll love..." \
-m "features-detail.jpg" \
-c "💰 Special offer: 50% off!" \
-m "discount.jpg" \
-i "twitter-123,linkedin-456"
```
### Example 2: Twitter Thread
```bash
postiz posts:create \
-c "🧵 Thread: How to X (1/5)" -m "intro.jpg" \
-c "Step 1: ... (2/5)" -m "step1.jpg" \
-c "Step 2: ... (3/5)" -m "step2.jpg" \
-c "Step 3: ... (4/5)" -m "step3.jpg" \
-c "Conclusion (5/5)" -m "done.jpg" \
-d 2000 \
-i "twitter-123"
```
### Example 3: Tutorial with Screenshots
```bash
postiz posts:create \
-c "Tutorial: Feature X 📖" \
-m "tutorial-cover.jpg" \
-c "1. Open settings" \
-m "settings-screenshot.jpg" \
-c "2. Enable feature X" \
-m "enable-screenshot.jpg" \
-c "3. You're done! 🎉" \
-m "success-screenshot.jpg" \
-i "twitter-123"
```
### Example 4: Content with Special Characters
```bash
postiz posts:create \
-c "Main post about programming" \
-c "First tip: Use const; avoid var" \
-c "Second tip: Functions should do one thing; keep it simple" \
-c "Third tip: Comments should explain 'why'; not 'what'" \
-i "twitter-123"
```
**No escaping needed!** Semicolons work perfectly.
## Options Reference
| Option | Alias | Multiple? | Description |
|--------|-------|-----------|-------------|
| `--content` | `-c` | ✅ Yes | Post/comment content |
| `--media` | `-m` | ✅ Yes | Comma-separated media URLs |
| `--integrations` | `-i` | ❌ No | Integration IDs |
| `--schedule` | `-s` | ❌ No | ISO 8601 date |
| `--delay` | `-d` | ❌ No | Delay between comments (ms, default: 5000) |
| `--shortLink` | - | ❌ No | Use URL shortener (default: true) |
| `--json` | `-j` | ❌ No | Load from JSON file |
## Delay Between Comments
Use `-d` to control the delay between comments:
```bash
postiz posts:create \
-c "Main" \
-c "Comment 1" \
-c "Comment 2" \
-d 10000 \ # 10 seconds between each
-i "twitter-123"
```
**Default:** 5000ms (5 seconds)
## Command Line vs JSON
### Use Command Line When:
- ✅ Quick posts
- ✅ Same content for all platforms
- ✅ Simple structure
- ✅ Dynamic/scripted content
### Use JSON When:
- ✅ Different content per platform
- ✅ Very complex structures
- ✅ Reusable templates
- ✅ Integration with other tools
## For AI Agents
### Generating Commands
```javascript
function buildPostCommand(posts, integrationId) {
const parts = ['postiz posts:create'];
posts.forEach(post => {
parts.push(`-c "${post.content.replace(/"/g, '\\"')}"`);
if (post.media && post.media.length > 0) {
parts.push(`-m "${post.media.join(',')}"`);
}
});
parts.push(`-i "${integrationId}"`);
return parts.join(' \\\n ');
}
// Usage
const posts = [
{ content: "Main post", media: ["img1.jpg", "img2.jpg"] },
{ content: "Comment; with semicolon!", media: ["img3.jpg"] },
{ content: "Another comment", media: [] }
];
const command = buildPostCommand(posts, "twitter-123");
console.log(command);
```
Output:
```bash
postiz posts:create \
-c "Main post" \
-m "img1.jpg,img2.jpg" \
-c "Comment; with semicolon!" \
-m "img3.jpg" \
-c "Another comment" \
-i "twitter-123"
```
## Migration Guide
If you have existing scripts using the old syntax:
### Before:
```bash
postiz posts:create \
-c "Main post" \
--comments "Comment 1;Comment 2" \
--image "main-image.jpg" \
-i "twitter-123"
```
### After:
```bash
postiz posts:create \
-c "Main post" -m "main-image.jpg" \
-c "Comment 1" \
-c "Comment 2" \
-i "twitter-123"
```
## Documentation
See these files for more details:
- **COMMAND_LINE_GUIDE.md** - Comprehensive command-line guide
- **command-line-examples.sh** - Executable examples
- **EXAMPLES.md** - Full usage patterns
- **SKILL.md** - AI agent integration
- **README.md** - General documentation
## Summary
### ✅ You Can Now:
1. **Use multiple `-c` flags** for main post + comments
2. **Use multiple `-m` flags** to pair media with each `-c`
3. **Use semicolons freely** in your content
4. **Create complex threads** easily from command line
5. **Each comment has its own media** array
6. **More intuitive syntax** overall
### 🎯 Perfect For:
- Twitter threads
- Product launches with follow-ups
- Tutorials with screenshots
- Event coverage
- Multi-step announcements
- Any post with comments that need their own media!
**The CLI is now much more powerful and user-friendly!** 🚀

View file

@ -1,358 +0,0 @@
# Postiz CLI - Command Line Guide
## New Syntax: Multiple `-c` and `-m` Flags
The CLI now supports a much more intuitive syntax for creating posts with comments that have their own media.
## Basic Syntax
```bash
postiz posts:create \
-c "content" -m "media" \ # Can be repeated multiple times
-c "content" -m "media" \ # Each pair = one post/comment
-i "integration-id"
```
### How It Works
- **First `-c`**: Main post content
- **Subsequent `-c`**: Comments/replies
- **Each `-m`**: Media for the corresponding `-c`
- `-m` is optional (text-only posts/comments)
- Order matters: `-c` and `-m` are paired in order
## Examples
### 1. Simple Post
```bash
postiz posts:create \
-c "Hello World!" \
-i "twitter-123"
```
### 2. Post with Multiple Images
```bash
postiz posts:create \
-c "Check out these photos!" \
-m "photo1.jpg,photo2.jpg,photo3.jpg" \
-i "twitter-123"
```
**Result:**
- Main post with 3 images
### 3. Post with Comments, Each Having Their Own Media
```bash
postiz posts:create \
-c "Main post 🚀" \
-m "main-image1.jpg,main-image2.jpg" \
-c "First comment 📸" \
-m "comment1-image.jpg" \
-c "Second comment 🎨" \
-m "comment2-img1.jpg,comment2-img2.jpg" \
-i "twitter-123"
```
**Result:**
- Main post with 2 images
- First comment (posted 5s later) with 1 image
- Second comment (posted 10s later) with 2 images
### 4. Comments Can Contain Semicolons! 🎉
```bash
postiz posts:create \
-c "Main post" \
-c "First comment; with a semicolon!" \
-c "Second comment; with multiple; semicolons; works fine!" \
-i "twitter-123"
```
**No escaping needed!** Each `-c` is a separate argument, so special characters work perfectly.
### 5. Twitter Thread
```bash
postiz posts:create \
-c "🧵 Thread about X (1/5)" \
-m "thread1.jpg" \
-c "Key point 1 (2/5)" \
-m "thread2.jpg" \
-c "Key point 2 (3/5)" \
-m "thread3.jpg" \
-c "Key point 3 (4/5)" \
-m "thread4.jpg" \
-c "Conclusion 🎉 (5/5)" \
-m "thread5.jpg" \
-d 2000 \
-i "twitter-123"
```
**Result:** 5-part thread with 2-second delays between tweets
### 6. Mix: Some with Media, Some Without
```bash
postiz posts:create \
-c "Amazing sunset! 🌅" \
-m "sunset.jpg" \
-c "Taken at 6:30 PM" \
-c "Location: Santa Monica Beach" \
-c "Camera: iPhone 15 Pro" \
-i "twitter-123"
```
**Result:**
- Main post with 1 image
- 3 text-only comments
### 7. Multi-Platform with Same Content
```bash
postiz posts:create \
-c "Big announcement! 🎉" \
-m "announcement.jpg" \
-c "More details coming soon..." \
-i "twitter-123,linkedin-456,facebook-789"
```
**Result:** Same post + comment posted to all 3 platforms
### 8. Scheduled Post with Follow-ups
```bash
postiz posts:create \
-c "Product launching today! 🚀" \
-m "product-hero.jpg,product-features.jpg" \
-c "Special launch offer: 50% off!" \
-m "discount-banner.jpg" \
-c "Limited to first 100 customers!" \
-s "2024-12-25T09:00:00Z" \
-i "twitter-123"
```
**Result:** Scheduled main post with 2 follow-up comments
### 9. Product Tutorial
```bash
postiz posts:create \
-c "Tutorial: How to Use Feature X 📖" \
-m "tutorial-intro.jpg" \
-c "Step 1: Open the settings menu" \
-m "step1-screenshot.jpg" \
-c "Step 2: Toggle the feature on" \
-m "step2-screenshot.jpg" \
-c "Step 3: Customize your preferences" \
-m "step3-screenshot.jpg" \
-c "That's it! You're all set 🎉" \
-d 3000 \
-i "twitter-123"
```
## Options Reference
| Flag | Alias | Description | Multiple? |
|------|-------|-------------|-----------|
| `--content` | `-c` | Post/comment content | ✅ Yes |
| `--media` | `-m` | Comma-separated media URLs | ✅ Yes |
| `--integrations` | `-i` | Comma-separated integration IDs | ❌ No |
| `--schedule` | `-s` | ISO 8601 date (schedule post) | ❌ No |
| `--delay` | `-d` | Delay between comments (ms) | ❌ No |
| `--shortLink` | - | Use URL shortener | ❌ No |
| `--json` | `-j` | Load from JSON file | ❌ No |
## How `-c` and `-m` Pair Together
```bash
postiz posts:create \
-c "First content" -m "first-media.jpg" \ # Pair 1 → Main post
-c "Second content" -m "second-media.jpg" \ # Pair 2 → Comment 1
-c "Third content" -m "third-media.jpg" \ # Pair 3 → Comment 2
-i "twitter-123"
```
**Pairing logic:**
- 1st `-c` pairs with 1st `-m` (if provided)
- 2nd `-c` pairs with 2nd `-m` (if provided)
- 3rd `-c` pairs with 3rd `-m` (if provided)
- If no `-m` for a `-c`, it's text-only
## Delay Between Comments
Use `-d` or `--delay` to set the delay (in milliseconds) between comments:
```bash
postiz posts:create \
-c "Main post" \
-c "Comment 1" \
-c "Comment 2" \
-d 10000 \ # 10 seconds between each
-i "twitter-123"
```
**Default:** 5000ms (5 seconds)
## Comparison: Old vs New Syntax
### ❌ Old Way (Limited)
```bash
# Could only do simple comments without custom media
postiz posts:create \
-c "Main post" \
--comments "Comment 1;Comment 2;Comment 3" \
--image "main-image.jpg" \
-i "twitter-123"
```
**Problems:**
- Comments couldn't have their own media
- Semicolons in content would break it
- Less intuitive
### ✅ New Way (Flexible)
```bash
postiz posts:create \
-c "Main post" -m "main.jpg" \
-c "Comment 1; with semicolon!" -m "comment1.jpg" \
-c "Comment 2" -m "comment2.jpg" \
-i "twitter-123"
```
**Benefits:**
- ✅ Each comment can have its own media
- ✅ Semicolons work fine
- ✅ More readable
- ✅ More flexible
## When to Use JSON vs Command Line
### Use Command Line (`-c` and `-m`) When:
- ✅ Same content for all integrations
- ✅ Simple, straightforward posts
- ✅ Quick one-off posts
- ✅ Scripting with dynamic content
### Use JSON (`--json`) When:
- ✅ Different content per platform
- ✅ Complex settings or metadata
- ✅ Reusable post templates
- ✅ Very long or formatted content
## Tips for AI Agents
### Generate Commands Programmatically
```javascript
function createThreadCommand(tweets, integrationId) {
const parts = [
'postiz posts:create'
];
tweets.forEach(tweet => {
parts.push(`-c "${tweet.content}"`);
if (tweet.media && tweet.media.length > 0) {
parts.push(`-m "${tweet.media.join(',')}"`);
}
});
parts.push(`-i "${integrationId}"`);
return parts.join(' \\\n ');
}
const thread = [
{ content: "Tweet 1/3", media: ["img1.jpg"] },
{ content: "Tweet 2/3", media: ["img2.jpg"] },
{ content: "Tweet 3/3", media: ["img3.jpg"] }
];
const command = createThreadCommand(thread, "twitter-123");
console.log(command);
```
### Escape Special Characters
In bash, you may need to escape some characters:
```bash
# Single quotes prevent interpolation
postiz posts:create \
-c 'Message with $variables and "quotes"' \
-i "twitter-123"
# Or use backslashes
postiz posts:create \
-c "Message with \$variables and \"quotes\"" \
-i "twitter-123"
```
## Error Handling
### Missing Integration
```bash
postiz posts:create -c "Post" -m "img.jpg"
# ❌ Error: --integrations is required when not using --json
```
**Fix:** Add `-i` flag
### No Content
```bash
postiz posts:create -i "twitter-123"
# ❌ Error: Either --content or --json is required
```
**Fix:** Add at least one `-c` flag
### Mismatched Count (OK!)
```bash
# This is fine! Extra -m flags are ignored
postiz posts:create \
-c "Post 1" -m "img1.jpg" \
-c "Post 2" \
-c "Post 3" -m "img3.jpg" \
-i "twitter-123"
# Result:
# - Post 1 with img1.jpg
# - Post 2 with no media
# - Post 3 with img3.jpg
```
## Full Example: Product Launch
```bash
#!/bin/bash
export POSTIZ_API_KEY=your_key
postiz posts:create \
-c "🚀 Launching ProductX today!" \
-m "https://cdn.example.com/hero.jpg,https://cdn.example.com/features.jpg" \
-c "🎯 Key Features:\n• AI-powered\n• Cloud-native\n• Open source" \
-m "https://cdn.example.com/features-detail.jpg" \
-c "💰 Special launch pricing: 50% off for early adopters!" \
-m "https://cdn.example.com/pricing.jpg" \
-c "🔗 Get started: https://example.com/productx" \
-s "2024-12-25T09:00:00Z" \
-d 3600000 \
-i "twitter-123,linkedin-456,facebook-789"
echo "✅ Product launch scheduled!"
```
## See Also
- **EXAMPLES.md** - JSON file examples
- **SKILL.md** - AI agent patterns
- **README.md** - Full documentation
- **examples/*.json** - Template files

View file

@ -1,316 +0,0 @@
# Postiz CLI - Advanced Examples
This directory contains examples demonstrating the full capabilities of the Postiz CLI, including posts with comments and multiple media.
## Understanding the Post Structure
The Postiz API supports a rich post structure:
```typescript
{
type: 'now' | 'schedule' | 'draft' | 'update',
date: string, // ISO 8601 date
shortLink: boolean, // Use URL shortener
tags: Tag[], // Post tags
posts: [ // Can post to multiple platforms at once
{
integration: { id: string }, // Platform integration ID
value: [ // Main post + comments/thread
{
content: string, // Post/comment text
image: MediaDto[], // Multiple media attachments
delay?: number // Delay in ms before posting (for comments)
},
// ... more comments
],
settings: { __type: 'EmptySettings' }
}
]
}
```
## Simple Usage Examples
### Basic Post
```bash
postiz posts:create \
-c "Hello World!" \
-i "twitter-123"
```
### Post with Multiple Images
```bash
postiz posts:create \
-c "Check out these images!" \
--image "https://example.com/img1.jpg,https://example.com/img2.jpg,https://example.com/img3.jpg" \
-i "twitter-123"
```
### Post with Comments (Simple)
```bash
postiz posts:create \
-c "Main post content" \
--comments "First comment;Second comment;Third comment" \
-i "twitter-123"
```
### Scheduled Post
```bash
postiz posts:create \
-c "Future post" \
-s "2024-12-31T12:00:00Z" \
-i "twitter-123,linkedin-456"
```
## Advanced JSON Examples
For complex posts with comments that have their own media, use JSON files:
### 1. Post with Comments and Media
**File:** `post-with-comments.json`
```bash
postiz posts:create --json examples/post-with-comments.json
```
This creates:
- Main post with 2 images
- First comment with 1 image (posted 5s after main)
- Second comment with 2 images (posted 10s after main)
### 2. Multi-Platform Campaign
**File:** `multi-platform-post.json`
```bash
postiz posts:create --json examples/multi-platform-post.json
```
This creates:
- Twitter post with main + comment
- LinkedIn post with single content
- Facebook post with main + comment
All scheduled for the same time with platform-specific content and media!
### 3. Twitter Thread
**File:** `thread-post.json`
```bash
postiz posts:create --json examples/thread-post.json
```
This creates a 5-part Twitter thread, with each tweet having its own image and a 2-second delay between tweets.
## JSON File Structure Explained
### Basic Structure
```json
{
"type": "now", // "now", "schedule", "draft", "update"
"date": "2024-01-15T12:00:00Z", // When to post (ISO 8601)
"shortLink": true, // Enable URL shortening
"tags": [], // Array of tags
"posts": [...] // Array of posts
}
```
### Post Structure
```json
{
"integration": {
"id": "twitter-123" // Get this from integrations:list
},
"value": [ // Array of content (main + comments)
{
"content": "Post text", // The actual content
"image": [ // Array of media
{
"id": "unique-id", // Unique identifier
"path": "https://..." // URL to the image
}
],
"delay": 5000 // Optional delay in milliseconds
}
],
"settings": {
"__type": "EmptySettings" // Platform-specific settings
}
}
```
## Use Cases
### 1. Product Launch Campaign
Create a coordinated multi-platform launch:
```json
{
"type": "schedule",
"date": "2024-03-15T09:00:00Z",
"posts": [
{
"integration": { "id": "twitter-id" },
"value": [
{ "content": "🚀 Launching today!", "image": [...] },
{ "content": "Special features:", "image": [...], "delay": 3600000 },
{ "content": "Get it now:", "image": [...], "delay": 7200000 }
]
},
{
"integration": { "id": "linkedin-id" },
"value": [
{ "content": "Professional announcement...", "image": [...] }
]
}
]
}
```
### 2. Tutorial Series
Create an educational thread:
```json
{
"type": "now",
"posts": [
{
"integration": { "id": "twitter-id" },
"value": [
{ "content": "🧵 How to X (1/5)", "image": [...] },
{ "content": "Step 1: ... (2/5)", "image": [...], "delay": 2000 },
{ "content": "Step 2: ... (3/5)", "image": [...], "delay": 2000 },
{ "content": "Step 3: ... (4/5)", "image": [...], "delay": 2000 },
{ "content": "Conclusion (5/5)", "image": [...], "delay": 2000 }
]
}
]
}
```
### 3. Event Coverage
Live event updates with media:
```json
{
"type": "now",
"posts": [
{
"integration": { "id": "twitter-id" },
"value": [
{
"content": "📍 Event starting now!",
"image": [
{ "id": "1", "path": "venue-photo.jpg" }
]
},
{
"content": "First speaker taking stage",
"image": [
{ "id": "2", "path": "speaker-photo.jpg" }
],
"delay": 1800000
}
]
}
]
}
```
## Getting Integration IDs
Before creating posts, get your integration IDs:
```bash
postiz integrations:list
```
Output:
```json
[
{ "id": "abc-123-twitter", "provider": "twitter", "name": "@myaccount" },
{ "id": "def-456-linkedin", "provider": "linkedin", "name": "My Company" }
]
```
Use these IDs in your `integration.id` fields.
## Tips for AI Agents
1. **Use JSON for complex posts** - If you need comments with media, always use JSON files
2. **Delays matter** - Use appropriate delays between comments (Twitter: 2-5s, others: 30s-1min)
3. **Image IDs** - Generate unique IDs for each image (can use UUIDs or random strings)
4. **Validate before sending** - Check that all integration IDs exist
5. **Test with "draft" type** - Use `"type": "draft"` to create without posting
## Automation Scripts
### Batch Create from Directory
```bash
#!/bin/bash
# Create posts from all JSON files in a directory
for file in posts/*.json; do
echo "Creating post from $file..."
postiz posts:create --json "$file"
sleep 2
done
```
### Generate JSON Programmatically
```javascript
const fs = require('fs');
function createThreadPost(tweets, integrationId) {
return {
type: 'now',
date: new Date().toISOString(),
shortLink: true,
tags: [],
posts: [{
integration: { id: integrationId },
value: tweets.map((tweet, i) => ({
content: tweet.content,
image: tweet.images || [],
delay: i === 0 ? undefined : 2000
})),
settings: { __type: 'EmptySettings' }
}]
};
}
const thread = createThreadPost([
{ content: 'Tweet 1', images: [...] },
{ content: 'Tweet 2', images: [...] },
{ content: 'Tweet 3', images: [...] }
], 'twitter-123');
fs.writeFileSync('thread.json', JSON.stringify(thread, null, 2));
```
## Error Handling
Common errors and solutions:
1. **Invalid integration ID** - Run `integrations:list` to get valid IDs
2. **Invalid image path** - Ensure images are accessible URLs or uploaded to Postiz first
3. **Missing required fields** - Check that `type`, `date`, `shortLink`, `tags`, and `posts` are all present
4. **Invalid date format** - Use ISO 8601 format: `YYYY-MM-DDTHH:mm:ssZ`
## Further Reading
- See `SKILL.md` for AI agent patterns
- See `README.md` for installation and setup
- See `QUICK_START.md` for basic usage

View file

@ -1,103 +0,0 @@
#!/usr/bin/env node
/**
* Example: Using Postiz CLI from an AI Agent (Node.js)
*
* This demonstrates how AI agents can programmatically use the Postiz CLI
* to schedule social media posts.
*/
const { execSync } = require('child_process');
// Configuration
const POSTIZ_API_KEY = process.env.POSTIZ_API_KEY;
if (!POSTIZ_API_KEY) {
console.error('❌ POSTIZ_API_KEY environment variable is required');
process.exit(1);
}
/**
* Execute a Postiz CLI command
*/
function runPostizCommand(command) {
try {
const output = execSync(`postiz ${command}`, {
env: { ...process.env, POSTIZ_API_KEY },
encoding: 'utf-8',
});
return JSON.parse(output);
} catch (error) {
console.error(`Command failed: ${command}`);
console.error(error.message);
throw error;
}
}
/**
* Main AI Agent workflow
*/
async function main() {
console.log('🤖 AI Agent: Starting social media scheduling workflow...\n');
try {
// Step 1: Get available integrations
console.log('📋 Fetching connected integrations...');
const integrations = runPostizCommand('integrations:list');
console.log(`Found ${integrations.length || 0} integrations\n`);
// Step 2: Create multiple scheduled posts
const posts = [
{
content: '🌅 Good morning! Starting the day with positive energy.',
schedule: getScheduledTime(9, 0), // 9 AM
},
{
content: '☕ Midday motivation: Keep pushing towards your goals!',
schedule: getScheduledTime(12, 0), // 12 PM
},
{
content: '🌙 Evening reflection: What did you accomplish today?',
schedule: getScheduledTime(20, 0), // 8 PM
},
];
console.log('📝 Creating scheduled posts...');
for (let i = 0; i < posts.length; i++) {
const post = posts[i];
console.log(` ${i + 1}. Creating post scheduled for ${post.schedule}...`);
const command = `posts:create -c "${post.content}" -s "${post.schedule}"`;
const result = runPostizCommand(command);
console.log(` ✅ Post created with ID: ${result.id || 'unknown'}`);
}
console.log('\n📊 Checking created posts...');
const postsList = runPostizCommand('posts:list -l 5');
console.log(`Total recent posts: ${postsList.total || 0}\n`);
console.log('✅ AI Agent workflow completed successfully!');
} catch (error) {
console.error('\n❌ AI Agent workflow failed:', error.message);
process.exit(1);
}
}
/**
* Helper: Get ISO 8601 timestamp for today at specific time
*/
function getScheduledTime(hours, minutes) {
const date = new Date();
date.setHours(hours, minutes, 0, 0);
// If time already passed today, schedule for tomorrow
if (date < new Date()) {
date.setDate(date.getDate() + 1);
}
return date.toISOString();
}
// Run the agent
main().catch(console.error);

View file

@ -1,42 +0,0 @@
#!/bin/bash
# Basic Postiz CLI Usage Example
# Make sure to set your API key first: export POSTIZ_API_KEY=your_key
echo "🚀 Postiz CLI Example Workflow"
echo ""
# Check if API key is set
if [ -z "$POSTIZ_API_KEY" ]; then
echo "❌ POSTIZ_API_KEY is not set!"
echo "Set it with: export POSTIZ_API_KEY=your_api_key"
exit 1
fi
echo "✅ API key is set"
echo ""
# 1. List integrations
echo "📋 Step 1: Listing connected integrations..."
postiz integrations:list
echo ""
# 2. Create a post
echo "📝 Step 2: Creating a test post..."
postiz posts:create \
-c "Hello from Postiz CLI! This is an automated test post." \
-s "$(date -u -v+1H +%Y-%m-%dT%H:%M:%SZ)" # Schedule 1 hour from now
echo ""
# 3. List posts
echo "📋 Step 3: Listing recent posts..."
postiz posts:list -l 5
echo ""
echo "✅ Example workflow completed!"
echo ""
echo "💡 Tips:"
echo " - Use -i flag to specify integrations when creating posts"
echo " - Upload images with: postiz upload ./path/to/image.png"
echo " - Delete posts with: postiz posts:delete <post-id>"
echo " - Get help: postiz --help"

View file

@ -1,153 +0,0 @@
#!/bin/bash
# Postiz CLI - Command Line Examples
# Demonstrating the new -c and -m flag syntax
echo "🚀 Postiz CLI Command Line Examples"
echo ""
# Make sure API key is set
if [ -z "$POSTIZ_API_KEY" ]; then
echo "❌ POSTIZ_API_KEY is not set!"
echo "Set it with: export POSTIZ_API_KEY=your_api_key"
exit 1
fi
echo "✅ API key is set"
echo ""
# Example 1: Simple post
echo "📝 Example 1: Simple post"
echo "Command:"
echo 'postiz posts:create -c "Hello World!" -i "twitter-123"'
echo ""
# Example 2: Post with multiple images
echo "📸 Example 2: Post with multiple images"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Check out these amazing photos!" \'
echo ' -m "photo1.jpg,photo2.jpg,photo3.jpg" \'
echo ' -i "twitter-123"'
echo ""
# Example 3: Post with comments, each having their own media
echo "💬 Example 3: Post with comments, each having different media"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Main post content 🚀" \'
echo ' -m "main-image1.jpg,main-image2.jpg" \'
echo ' -c "First comment with its own image 📸" \'
echo ' -m "comment1-image.jpg" \'
echo ' -c "Second comment with different images 🎨" \'
echo ' -m "comment2-image1.jpg,comment2-image2.jpg" \'
echo ' -i "twitter-123"'
echo ""
# Example 4: Comments with semicolons (no escaping needed!)
echo "🎯 Example 4: Comments can contain semicolons!"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Main post" \'
echo ' -c "First comment; notice the semicolon!" \'
echo ' -c "Second comment; with multiple; semicolons; works fine!" \'
echo ' -i "twitter-123"'
echo ""
# Example 5: Twitter thread with custom delay
echo "🧵 Example 5: Twitter thread with 2-second delays"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "🧵 How to use Postiz CLI (1/5)" \'
echo ' -m "thread-intro.jpg" \'
echo ' -c "Step 1: Install the CLI (2/5)" \'
echo ' -m "step1-screenshot.jpg" \'
echo ' -c "Step 2: Set your API key (3/5)" \'
echo ' -m "step2-screenshot.jpg" \'
echo ' -c "Step 3: Create your first post (4/5)" \'
echo ' -m "step3-screenshot.jpg" \'
echo ' -c "You'\''re all set! 🎉 (5/5)" \'
echo ' -m "done.jpg" \'
echo ' -d 2000 \'
echo ' -i "twitter-123"'
echo ""
# Example 6: Scheduled post with comments
echo "⏰ Example 6: Scheduled post with follow-up comments"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Product launch! 🚀" \'
echo ' -m "product-hero.jpg,product-features.jpg" \'
echo ' -c "Special launch offer - 50% off!" \'
echo ' -m "discount-banner.jpg" \'
echo ' -c "Limited time only!" \'
echo ' -s "2024-12-25T09:00:00Z" \'
echo ' -i "twitter-123,linkedin-456"'
echo ""
# Example 7: Multi-platform with same content
echo "🌐 Example 7: Multi-platform posting"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Exciting announcement! 🎉" \'
echo ' -m "announcement.jpg" \'
echo ' -c "More details in the comments..." \'
echo ' -m "details-infographic.jpg" \'
echo ' -i "twitter-123,linkedin-456,facebook-789"'
echo ""
# Example 8: Comments without media
echo "💭 Example 8: Main post with media, comments without media"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Check out this amazing view! 🏔️" \'
echo ' -m "mountain-photo.jpg" \'
echo ' -c "Taken at sunrise this morning" \'
echo ' -c "Location: Swiss Alps" \'
echo ' -i "twitter-123"'
echo ""
# Example 9: Product tutorial series
echo "📚 Example 9: Product tutorial series"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Tutorial: Getting Started with Our Product 📖" \'
echo ' -m "tutorial-cover.jpg" \'
echo ' -c "1. First, download and install the app" \'
echo ' -m "install-screen.jpg" \'
echo ' -c "2. Create your account and set up your profile" \'
echo ' -m "signup-screen.jpg" \'
echo ' -c "3. You'\''re ready to go! Start creating your first project" \'
echo ' -m "dashboard-screen.jpg" \'
echo ' -d 3000 \'
echo ' -i "twitter-123"'
echo ""
# Example 10: Event coverage
echo "📍 Example 10: Live event coverage"
echo "Command:"
echo 'postiz posts:create \'
echo ' -c "Conference 2024 is starting! 🎤" \'
echo ' -m "venue-photo.jpg" \'
echo ' -c "First speaker: Jane Doe talking about AI" \'
echo ' -m "speaker1-photo.jpg" \'
echo ' -c "Second speaker: John Smith on cloud architecture" \'
echo ' -m "speaker2-photo.jpg" \'
echo ' -c "Networking break! Great conversations happening" \'
echo ' -m "networking-photo.jpg" \'
echo ' -d 30000 \'
echo ' -i "twitter-123,linkedin-456"'
echo ""
echo "💡 Tips:"
echo " - Use multiple -c flags for main post + comments"
echo " - Use -m flags to specify media for each -c"
echo " - First -c is the main post, subsequent ones are comments"
echo " - -m is optional, can be omitted for text-only comments"
echo " - Use -d to set delay between comments (in milliseconds)"
echo " - Semicolons and special characters work fine in -c content!"
echo ""
echo "📖 For more examples, see:"
echo " - examples/EXAMPLES.md - Comprehensive guide"
echo " - examples/*.json - JSON file examples"
echo " - SKILL.md - AI agent patterns"

View file

@ -1,89 +0,0 @@
{
"type": "schedule",
"date": "2024-12-25T12:00:00Z",
"shortLink": true,
"tags": [
{
"value": "holiday",
"label": "Holiday"
},
{
"value": "marketing",
"label": "Marketing"
}
],
"posts": [
{
"integration": {
"id": "twitter-integration-id"
},
"value": [
{
"content": "Happy Holidays! 🎄 Check out our special offers!",
"image": [
{
"id": "holiday1",
"path": "https://example.com/holiday-twitter.jpg"
}
]
},
{
"content": "Limited time offer - 50% off! 🎁",
"image": [],
"delay": 3600000
}
],
"settings": {
"__type": "EmptySettings"
}
},
{
"integration": {
"id": "linkedin-integration-id"
},
"value": [
{
"content": "Season's greetings from our team! We're offering exclusive holiday promotions.",
"image": [
{
"id": "holiday2",
"path": "https://example.com/holiday-linkedin.jpg"
}
]
}
],
"settings": {
"__type": "EmptySettings"
}
},
{
"integration": {
"id": "facebook-integration-id"
},
"value": [
{
"content": "🎅 Happy Holidays! Special announcement in the comments!",
"image": [
{
"id": "holiday3",
"path": "https://example.com/holiday-facebook-main.jpg"
}
]
},
{
"content": "Our holiday sale is now live! Visit our website for amazing deals 🎁",
"image": [
{
"id": "holiday4",
"path": "https://example.com/holiday-sale-banner.jpg"
}
],
"delay": 300000
}
],
"settings": {
"__type": "EmptySettings"
}
}
]
}

View file

@ -1,95 +0,0 @@
{
"type": "schedule",
"date": "2024-03-15T09:00:00Z",
"shortLink": true,
"tags": [
{ "value": "product-launch", "label": "Product Launch" }
],
"posts": [
{
"integration": { "id": "reddit-integration-id" },
"value": [{
"content": "We're launching our new CLI tool today!\n\nIt's designed to make social media scheduling effortless for developers and AI agents. Built with TypeScript, supports 28+ platforms, and has a clean, intuitive API.\n\nFeatures:\n- Multi-platform posting\n- Thread creation\n- Scheduled posts\n- Comments with media\n- Provider-specific settings\n\nTry it out and let us know what you think!",
"image": [
{ "id": "r1", "path": "https://cdn.example.com/reddit-screenshot.jpg" }
]
}],
"settings": {
"__type": "reddit",
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "Launching Postiz CLI - Social Media Automation for Developers",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}
},
{
"integration": { "id": "twitter-integration-id" },
"value": [
{
"content": "🚀 Launching Postiz CLI today!\n\nFinally, a developer-friendly way to automate social media. Built with TypeScript, supports 28+ platforms.\n\n✨ Features in thread below 👇",
"image": [
{ "id": "t1", "path": "https://cdn.example.com/twitter-banner.jpg" }
]
},
{
"content": "1⃣ Multi-platform posting\nPost to Twitter, LinkedIn, Reddit, TikTok, YouTube, and 23 more platforms with a single command",
"image": [
{ "id": "t2", "path": "https://cdn.example.com/multi-platform.jpg" }
],
"delay": 3000
},
{
"content": "2⃣ Thread creation\nEasily create Twitter threads, each tweet with its own media",
"image": [
{ "id": "t3", "path": "https://cdn.example.com/threads.jpg" }
],
"delay": 3000
},
{
"content": "3⃣ Provider-specific settings\nReddit subreddits, YouTube visibility, TikTok privacy - all configurable\n\nGet started: https://github.com/yourrepo",
"image": [],
"delay": 3000
}
],
"settings": {
"__type": "x",
"who_can_reply_post": "everyone"
}
},
{
"integration": { "id": "linkedin-integration-id" },
"value": [{
"content": "Excited to announce the launch of Postiz CLI! 🎉\n\nAs developers, we know how time-consuming social media management can be. That's why we built a powerful CLI tool that makes scheduling posts across 28+ platforms effortless.\n\nKey features:\n• Multi-platform support (Twitter, LinkedIn, Reddit, TikTok, YouTube, and more)\n• Thread and carousel creation\n• Scheduled posting with precise timing\n• Provider-specific settings and customization\n• Built for AI agents and automation\n\nWhether you're managing a personal brand, running marketing campaigns, or building AI-powered social media tools, Postiz CLI has you covered.\n\nCheck it out and let us know your thoughts!",
"image": [
{ "id": "l1", "path": "https://cdn.example.com/linkedin-slide1.jpg" },
{ "id": "l2", "path": "https://cdn.example.com/linkedin-slide2.jpg" },
{ "id": "l3", "path": "https://cdn.example.com/linkedin-slide3.jpg" }
]
}],
"settings": {
"__type": "linkedin",
"post_as_images_carousel": true,
"carousel_name": "Postiz CLI Launch"
}
},
{
"integration": { "id": "instagram-integration-id" },
"value": [{
"content": "🚀 New launch alert!\n\nPostiz CLI is here - automate your social media like a pro.\n\n✨ 28+ platforms\n📅 Scheduled posting\n🧵 Thread creation\n⚙ Full customization\n\nLink in bio! #developer #automation #socialmedia #tech",
"image": [
{ "id": "i1", "path": "https://cdn.example.com/instagram-post.jpg" }
]
}],
"settings": {
"__type": "instagram",
"post_type": "post",
"is_trial_reel": false
}
}
]
}

View file

@ -1,55 +0,0 @@
{
"type": "now",
"date": "2024-01-15T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [
{
"integration": {
"id": "your-integration-id-here"
},
"value": [
{
"content": "This is the main post content 🚀",
"image": [
{
"id": "img1",
"path": "https://example.com/main-image.jpg"
},
{
"id": "img2",
"path": "https://example.com/secondary-image.jpg"
}
]
},
{
"content": "This is the first comment with its own media 📸",
"image": [
{
"id": "img3",
"path": "https://example.com/comment1-image.jpg"
}
],
"delay": 5000
},
{
"content": "This is the second comment with different media 🎨",
"image": [
{
"id": "img4",
"path": "https://example.com/comment2-image1.jpg"
},
{
"id": "img5",
"path": "https://example.com/comment2-image2.jpg"
}
],
"delay": 10000
}
],
"settings": {
"__type": "EmptySettings"
}
}
]
}

View file

@ -1,27 +0,0 @@
{
"type": "now",
"date": "2024-01-15T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [{
"integration": {
"id": "your-reddit-integration-id"
},
"value": [{
"content": "I built a CLI tool for Postiz that makes social media scheduling super easy!\n\nYou can create posts, schedule them, and even post to multiple platforms at once. It supports comments with their own media, threads, and much more.\n\nCheck it out and let me know what you think!",
"image": []
}],
"settings": {
"__type": "reddit",
"subreddit": [{
"value": {
"subreddit": "programming",
"title": "Built a CLI tool for social media scheduling with TypeScript",
"type": "text",
"url": "",
"is_flair_required": false
}
}]
}
}]
}

View file

@ -1,67 +0,0 @@
{
"type": "now",
"date": "2024-01-15T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [
{
"integration": {
"id": "twitter-integration-id"
},
"value": [
{
"content": "🧵 Thread: How to use Postiz CLI for automated social media posting (1/5)",
"image": [
{
"id": "tutorial1",
"path": "https://example.com/tutorial-intro.jpg"
}
]
},
{
"content": "Step 1: Install the CLI and set your API key\n\nexport POSTIZ_API_KEY=your_key\npnpm install -g postiz (2/5)",
"image": [
{
"id": "tutorial2",
"path": "https://example.com/tutorial-install.jpg"
}
],
"delay": 2000
},
{
"content": "Step 2: List your connected integrations to get their IDs\n\npostiz integrations:list (3/5)",
"image": [
{
"id": "tutorial3",
"path": "https://example.com/tutorial-integrations.jpg"
}
],
"delay": 2000
},
{
"content": "Step 3: Create your first post\n\npostiz posts:create -c \"Hello World!\" -i \"twitter-123\" (4/5)",
"image": [
{
"id": "tutorial4",
"path": "https://example.com/tutorial-create.jpg"
}
],
"delay": 2000
},
{
"content": "That's it! You can now automate your social media posts with ease. Check out our docs for more advanced features! 🚀 (5/5)",
"image": [
{
"id": "tutorial5",
"path": "https://example.com/tutorial-done.jpg"
}
],
"delay": 2000
}
],
"settings": {
"__type": "EmptySettings"
}
}
]
}

View file

@ -1,31 +0,0 @@
{
"type": "now",
"date": "2024-01-15T12:00:00Z",
"shortLink": true,
"tags": [],
"posts": [{
"integration": {
"id": "your-tiktok-integration-id"
},
"value": [{
"content": "Quick tip: Automate your social media with this CLI tool! 🚀\n\n#coding #programming #typescript #developer #tech",
"image": [{
"id": "video1",
"path": "https://cdn.example.com/tiktok-video.mp4"
}]
}],
"settings": {
"__type": "tiktok",
"title": "Automate Social Media with CLI",
"privacy_level": "PUBLIC_TO_EVERYONE",
"duet": true,
"stitch": true,
"comment": true,
"autoAddMusic": "no",
"brand_content_toggle": false,
"brand_organic_toggle": false,
"video_made_with_ai": false,
"content_posting_method": "DIRECT_POST"
}
}]
}

View file

@ -1,34 +0,0 @@
{
"type": "schedule",
"date": "2024-12-25T09:00:00Z",
"shortLink": true,
"tags": [
{ "value": "tutorial", "label": "Tutorial" },
{ "value": "tech", "label": "Tech" }
],
"posts": [{
"integration": {
"id": "your-youtube-integration-id"
},
"value": [{
"content": "In this video, I'll show you how to build a powerful CLI tool for social media automation.\n\n⏱ Timestamps:\n0:00 - Introduction\n2:15 - Setting up the project\n5:30 - Building the API client\n10:45 - Creating commands\n15:20 - Testing and deployment\n\n📚 Resources:\n- GitHub: https://github.com/yourrepo\n- Documentation: https://docs.example.com\n\n🔔 Subscribe for more TypeScript tutorials!",
"image": [{
"id": "thumbnail1",
"path": "https://cdn.example.com/thumbnail.jpg"
}]
}],
"settings": {
"__type": "youtube",
"title": "Building a Social Media CLI Tool with TypeScript",
"type": "public",
"selfDeclaredMadeForKids": "no",
"tags": [
{ "value": "typescript", "label": "TypeScript" },
{ "value": "cli", "label": "CLI" },
{ "value": "tutorial", "label": "Tutorial" },
{ "value": "programming", "label": "Programming" },
{ "value": "nodejs", "label": "Node.js" }
]
}
}]
}

View file

@ -1,40 +0,0 @@
{
"name": "postiz",
"version": "2.0.5",
"description": "Postiz CLI - Command line interface for the Postiz social media scheduling API",
"main": "dist/index.js",
"bin": {
"postiz": "./dist/index.js"
},
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"start": "node ./dist/index.js",
"publish": "tsup && pnpm publish --access public --no-git-checks"
},
"files": [
"dist",
"README.md",
"SKILL.md"
],
"keywords": [
"postiz",
"cli",
"social media",
"scheduling",
"automation",
"ai-agent",
"command-line"
],
"author": "Nevo David",
"license": "AGPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/gitroomhq/postiz-app.git",
"directory": "apps/cli"
},
"homepage": "https://postiz.com",
"bugs": {
"url": "https://github.com/gitroomhq/postiz-app/issues"
}
}

View file

@ -1,162 +0,0 @@
import fetch, { FormData } from 'node-fetch';
export interface PostizConfig {
apiKey: string;
apiUrl?: string;
}
export class PostizAPI {
private apiKey: string;
private apiUrl: string;
constructor(config: PostizConfig) {
this.apiKey = config.apiKey;
this.apiUrl = config.apiUrl || 'https://api.postiz.com';
}
private async request(endpoint: string, options: any = {}) {
const url = `${this.apiUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
Authorization: this.apiKey,
...options.headers,
};
try {
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const error = await response.text();
throw new Error(`API Error (${response.status}): ${error}`);
}
return await response.json();
} catch (error: any) {
throw new Error(`Request failed: ${error.message}`);
}
}
async createPost(data: any) {
return this.request('/public/v1/posts', {
method: 'POST',
body: JSON.stringify(data),
});
}
async listPosts(filters: any = {}) {
const queryString = new URLSearchParams(
Object.entries(filters).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null) {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>)
).toString();
const endpoint = queryString
? `/public/v1/posts?${queryString}`
: '/public/v1/posts';
return this.request(endpoint, {
method: 'GET',
});
}
async deletePost(id: string) {
return this.request(`/public/v1/posts/${id}`, {
method: 'DELETE',
});
}
async upload(file: Buffer, filename: string) {
const formData = new FormData();
const extension = filename.split('.').pop()?.toLowerCase() || '';
// Determine MIME type based on file extension
const mimeTypes: Record<string, string> = {
// Images
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'webp': 'image/webp',
'svg': 'image/svg+xml',
'bmp': 'image/bmp',
'ico': 'image/x-icon',
// Videos
'mp4': 'video/mp4',
'mov': 'video/quicktime',
'avi': 'video/x-msvideo',
'mkv': 'video/x-matroska',
'webm': 'video/webm',
'flv': 'video/x-flv',
'wmv': 'video/x-ms-wmv',
'm4v': 'video/x-m4v',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'3gp': 'video/3gpp',
// Audio
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'ogg': 'audio/ogg',
'aac': 'audio/aac',
'flac': 'audio/flac',
'm4a': 'audio/mp4',
// Documents
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};
const type = mimeTypes[extension] || 'application/octet-stream';
const blob = new Blob([file], { type });
formData.append('file', blob, filename);
const url = `${this.apiUrl}/public/v1/upload`;
const response = await fetch(url, {
method: 'POST',
// @ts-ignore
body: formData,
headers: {
Authorization: this.apiKey,
},
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Upload failed (${response.status}): ${error}`);
}
return await response.json();
}
async listIntegrations() {
return this.request('/public/v1/integrations', {
method: 'GET',
});
}
async getIntegrationSettings(integrationId: string) {
return this.request(`/public/v1/integration-settings/${integrationId}`, {
method: 'GET',
});
}
async triggerIntegrationTool(
integrationId: string,
methodName: string,
data: Record<string, string>
) {
return this.request(`/public/v1/integration-trigger/${integrationId}`, {
method: 'POST',
body: JSON.stringify({ methodName, data }),
});
}
}

View file

@ -1,73 +0,0 @@
import { PostizAPI } from '../api';
import { getConfig } from '../config';
export async function listIntegrations() {
const config = getConfig();
const api = new PostizAPI(config);
try {
const result = await api.listIntegrations();
console.log('🔌 Connected Integrations:');
console.log(JSON.stringify(result, null, 2));
return result;
} catch (error: any) {
console.error('❌ Failed to list integrations:', error.message);
process.exit(1);
}
}
export async function getIntegrationSettings(args: any) {
const config = getConfig();
const api = new PostizAPI(config);
if (!args.id) {
console.error('❌ Integration ID is required');
process.exit(1);
}
try {
const result = await api.getIntegrationSettings(args.id);
console.log(`⚙️ Settings for integration: ${args.id}`);
console.log(JSON.stringify(result, null, 2));
return result;
} catch (error: any) {
console.error('❌ Failed to get integration settings:', error.message);
process.exit(1);
}
}
export async function triggerIntegrationTool(args: any) {
const config = getConfig();
const api = new PostizAPI(config);
if (!args.id) {
console.error('❌ Integration ID is required');
process.exit(1);
}
if (!args.method) {
console.error('❌ Method name is required');
process.exit(1);
}
// Parse data from JSON string or use empty object
let data: Record<string, string> = {};
if (args.data) {
try {
data = JSON.parse(args.data);
} catch (error: any) {
console.error('❌ Failed to parse data JSON:', error.message);
process.exit(1);
}
}
try {
const result = await api.triggerIntegrationTool(args.id, args.method, data);
console.log(`🔧 Tool result for ${args.method}:`);
console.log(JSON.stringify(result, null, 2));
return result;
} catch (error: any) {
console.error('❌ Failed to trigger tool:', error.message);
process.exit(1);
}
}

View file

@ -1,155 +0,0 @@
import { PostizAPI } from '../api';
import { getConfig } from '../config';
import { readFileSync, existsSync } from 'fs';
export async function createPost(args: any) {
const config = getConfig();
const api = new PostizAPI(config);
// Support both simple and complex post creation
let postData: any;
if (args.json) {
// Load from JSON file for complex posts with comments and media
try {
const jsonPath = args.json;
if (!existsSync(jsonPath)) {
console.error(`❌ JSON file not found: ${jsonPath}`);
process.exit(1);
}
const jsonContent = readFileSync(jsonPath, 'utf-8');
postData = JSON.parse(jsonContent);
} catch (error: any) {
console.error('❌ Failed to parse JSON file:', error.message);
process.exit(1);
}
} else {
const integrations = args.integrations
? args.integrations.split(',').map((id: string) => id.trim())
: [];
if (integrations.length === 0) {
console.error('❌ At least one integration ID is required');
console.error('Use -i or --integrations to specify integration IDs');
console.error('Run "postiz integrations:list" to see available integrations');
process.exit(1);
}
// Support multiple -c and -m flags
// Normalize to arrays
const contents = Array.isArray(args.content) ? args.content : [args.content];
const medias = Array.isArray(args.media) ? args.media : (args.media ? [args.media] : []);
if (!contents[0]) {
console.error('❌ At least one -c/--content is required');
process.exit(1);
}
// Build value array by pairing contents with their media
const values = contents.map((content: string, index: number) => {
const mediaForThisContent = medias[index];
const images = mediaForThisContent
? mediaForThisContent.split(',').map((img: string) => ({
id: Math.random().toString(36).substring(7),
path: img.trim(),
}))
: [];
return {
content: content,
image: images,
// Add delay for all items except the first (main post)
...(index > 0 && { delay: args.delay || 5000 }),
};
});
// Parse provider-specific settings if provided
// Note: __type is automatically added by the backend based on integration ID
let settings: any = undefined;
if (args.settings) {
try {
settings = typeof args.settings === 'string'
? JSON.parse(args.settings)
: args.settings;
} catch (error: any) {
console.error('❌ Failed to parse settings JSON:', error.message);
process.exit(1);
}
}
// Build the proper post structure
postData = {
type: args.type || 'schedule', // 'schedule' or 'draft'
date: args.date, // Required date field
shortLink: args.shortLink !== false,
tags: [],
posts: integrations.map((integrationId: string) => ({
integration: { id: integrationId },
value: values,
settings: settings,
})),
};
}
try {
const result = await api.createPost(postData);
console.log('✅ Post created successfully!');
console.log(JSON.stringify(result, null, 2));
return result;
} catch (error: any) {
console.error('❌ Failed to create post:', error.message);
process.exit(1);
}
}
export async function listPosts(args: any) {
const config = getConfig();
const api = new PostizAPI(config);
// Set default date range: last 30 days to 30 days in the future
const defaultStartDate = new Date();
defaultStartDate.setDate(defaultStartDate.getDate() - 30);
const defaultEndDate = new Date();
defaultEndDate.setDate(defaultEndDate.getDate() + 30);
// Only send fields that are in GetPostsDto
const filters: any = {
startDate: args.startDate || defaultStartDate.toISOString(),
endDate: args.endDate || defaultEndDate.toISOString(),
};
// customer is optional in the DTO
if (args.customer) {
filters.customer = args.customer;
}
try {
const result = await api.listPosts(filters);
console.log('📋 Posts:');
console.log(JSON.stringify(result, null, 2));
return result;
} catch (error: any) {
console.error('❌ Failed to list posts:', error.message);
process.exit(1);
}
}
export async function deletePost(args: any) {
const config = getConfig();
const api = new PostizAPI(config);
if (!args.id) {
console.error('❌ Post ID is required');
process.exit(1);
}
try {
await api.deletePost(args.id);
console.log(`✅ Post ${args.id} deleted successfully!`);
} catch (error: any) {
console.error('❌ Failed to delete post:', error.message);
process.exit(1);
}
}

View file

@ -1,26 +0,0 @@
import { PostizAPI } from '../api';
import { getConfig } from '../config';
import { readFileSync } from 'fs';
export async function uploadFile(args: any) {
const config = getConfig();
const api = new PostizAPI(config);
if (!args.file) {
console.error('❌ File path is required');
process.exit(1);
}
try {
const fileBuffer = readFileSync(args.file);
const filename = args.file.split('/').pop() || 'file';
const result = await api.upload(fileBuffer, filename);
console.log('✅ File uploaded successfully!');
console.log(JSON.stringify(result, null, 2));
return result;
} catch (error: any) {
console.error('❌ Failed to upload file:', error.message);
process.exit(1);
}
}

View file

@ -1,17 +0,0 @@
import { PostizConfig } from './api';
export function getConfig(): PostizConfig {
const apiKey = process.env.POSTIZ_API_KEY;
const apiUrl = process.env.POSTIZ_API_URL;
if (!apiKey) {
console.error('❌ Error: POSTIZ_API_KEY environment variable is required');
console.error('Please set it using: export POSTIZ_API_KEY=your_api_key');
process.exit(1);
}
return {
apiKey,
apiUrl,
};
}

View file

@ -1,240 +0,0 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { createPost, listPosts, deletePost } from './commands/posts';
import { listIntegrations, getIntegrationSettings, triggerIntegrationTool } from './commands/integrations';
import { uploadFile } from './commands/upload';
import type { Argv } from 'yargs';
yargs(hideBin(process.argv))
.scriptName('postiz')
.usage('$0 <command> [options]')
.command(
'posts:create',
'Create a new post',
(yargs: Argv) => {
return yargs
.option('content', {
alias: 'c',
describe: 'Post/comment content (can be used multiple times)',
type: 'string',
})
.option('media', {
alias: 'm',
describe: 'Comma-separated media URLs for the corresponding -c (can be used multiple times)',
type: 'string',
})
.option('integrations', {
alias: 'i',
describe: 'Comma-separated list of integration IDs',
type: 'string',
})
.option('date', {
alias: 's',
describe: 'Schedule date (ISO 8601 format) - REQUIRED',
type: 'string',
})
.option('type', {
alias: 't',
describe: 'Post type: "schedule" or "draft"',
type: 'string',
choices: ['schedule', 'draft'],
default: 'schedule',
})
.option('delay', {
alias: 'd',
describe: 'Delay in milliseconds between comments (default: 5000)',
type: 'number',
default: 5000,
})
.option('json', {
alias: 'j',
describe: 'Path to JSON file with full post structure',
type: 'string',
})
.option('shortLink', {
describe: 'Use short links',
type: 'boolean',
default: true,
})
.option('settings', {
describe: 'Platform-specific settings as JSON string',
type: 'string',
})
.check((argv) => {
if (!argv.json && !argv.content) {
throw new Error('Either --content or --json is required');
}
if (!argv.json && !argv.integrations) {
throw new Error('--integrations is required when not using --json');
}
if (!argv.json && !argv.date) {
throw new Error('--date is required when not using --json');
}
return true;
})
.example(
'$0 posts:create -c "Hello World!" -s "2024-12-31T12:00:00Z" -i "twitter-123"',
'Simple scheduled post'
)
.example(
'$0 posts:create -c "Draft post" -s "2024-12-31T12:00:00Z" -t draft -i "twitter-123"',
'Create draft post'
)
.example(
'$0 posts:create -c "Main post" -m "img1.jpg,img2.jpg" -s "2024-12-31T12:00:00Z" -i "twitter-123"',
'Post with multiple images'
)
.example(
'$0 posts:create -c "Main post" -m "img1.jpg" -c "First comment" -m "img2.jpg" -c "Second comment" -m "img3.jpg,img4.jpg" -s "2024-12-31T12:00:00Z" -i "twitter-123"',
'Post with comments, each having their own media'
)
.example(
'$0 posts:create -c "Main" -c "Comment with semicolon; see?" -c "Another!" -s "2024-12-31T12:00:00Z" -i "twitter-123"',
'Comments can contain semicolons'
)
.example(
'$0 posts:create -c "Thread 1/3" -c "Thread 2/3" -c "Thread 3/3" -d 2000 -s "2024-12-31T12:00:00Z" -i "twitter-123"',
'Twitter thread with 2s delay'
)
.example(
'$0 posts:create --json ./post.json',
'Complex post from JSON file'
)
.example(
'$0 posts:create -c "Post to subreddit" -s "2024-12-31T12:00:00Z" --settings \'{"subreddit":[{"value":{"subreddit":"programming","title":"My Title","type":"text","url":"","is_flair_required":false}}]}\' -i "reddit-123"',
'Reddit post with specific subreddit settings'
)
.example(
'$0 posts:create -c "Video description" -s "2024-12-31T12:00:00Z" --settings \'{"title":"My Video","type":"public","tags":[{"value":"tech","label":"Tech"}]}\' -i "youtube-123"',
'YouTube post with title and tags'
)
.example(
'$0 posts:create -c "Tweet content" -s "2024-12-31T12:00:00Z" --settings \'{"who_can_reply_post":"everyone"}\' -i "twitter-123"',
'X (Twitter) post with reply settings'
);
},
createPost as any
)
.command(
'posts:list',
'List all posts',
(yargs: Argv) => {
return yargs
.option('startDate', {
describe: 'Start date (ISO 8601 format). Default: 30 days ago',
type: 'string',
})
.option('endDate', {
describe: 'End date (ISO 8601 format). Default: 30 days from now',
type: 'string',
})
.option('customer', {
describe: 'Customer ID (optional)',
type: 'string',
})
.example('$0 posts:list', 'List all posts (last 30 days to next 30 days)')
.example(
'$0 posts:list --startDate "2024-01-01T00:00:00Z" --endDate "2024-12-31T23:59:59Z"',
'List posts for a specific date range'
)
.example(
'$0 posts:list --customer "customer-id"',
'List posts for a specific customer'
);
},
listPosts as any
)
.command(
'posts:delete <id>',
'Delete a post',
(yargs: Argv) => {
return yargs
.positional('id', {
describe: 'Post ID to delete',
type: 'string',
})
.example('$0 posts:delete abc123', 'Delete post with ID abc123');
},
deletePost as any
)
.command(
'integrations:list',
'List all connected integrations',
{},
listIntegrations as any
)
.command(
'integrations:settings <id>',
'Get settings schema for a specific integration',
(yargs: Argv) => {
return yargs
.positional('id', {
describe: 'Integration ID',
type: 'string',
})
.example(
'$0 integrations:settings reddit-123',
'Get settings schema for Reddit integration'
)
.example(
'$0 integrations:settings youtube-456',
'Get settings schema for YouTube integration'
);
},
getIntegrationSettings as any
)
.command(
'integrations:trigger <id> <method>',
'Trigger an integration tool to fetch additional data',
(yargs: Argv) => {
return yargs
.positional('id', {
describe: 'Integration ID',
type: 'string',
})
.positional('method', {
describe: 'Method name from the integration tools',
type: 'string',
})
.option('data', {
alias: 'd',
describe: 'Data to pass to the tool as JSON string',
type: 'string',
})
.example(
'$0 integrations:trigger reddit-123 getSubreddits',
'Get list of subreddits'
)
.example(
'$0 integrations:trigger reddit-123 searchSubreddits -d \'{"query":"programming"}\'',
'Search for subreddits'
)
.example(
'$0 integrations:trigger youtube-123 getPlaylists',
'Get YouTube playlists'
);
},
triggerIntegrationTool as any
)
.command(
'upload <file>',
'Upload a file',
(yargs: Argv) => {
return yargs
.positional('file', {
describe: 'File path to upload',
type: 'string',
})
.example('$0 upload ./image.png', 'Upload an image');
},
uploadFile as any
)
.demandCommand(1, 'You need at least one command')
.help()
.alias('h', 'help')
.version()
.alias('v', 'version')
.epilogue(
'For more information, visit: https://postiz.com\n\nSet your API key: export POSTIZ_API_KEY=your_api_key'
)
.parse();

View file

@ -1,15 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"esModuleInterop": true,
"rootDir": "../../",
"incremental": false
},
"include": ["src"]
}

View file

@ -1,14 +0,0 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs'],
dts: false, // Disable DTS generation to avoid type issues
splitting: false,
sourcemap: true,
clean: true,
outDir: 'dist',
banner: {
js: '#!/usr/bin/env node',
},
});

View file

@ -0,0 +1,18 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";
const eslintConfig = [
...nextCoreWebVitals,
...nextTypescript,
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;

View file

@ -8,41 +8,32 @@ const nextConfig = {
},
// Document-Policy header for browser profiling
async headers() {
return [{
source: "/:path*",
headers: [{
key: "Document-Policy",
value: "js-profiling",
}, ],
}, ];
return [
{
source: '/:path*',
headers: [
{
key: 'Document-Policy',
value: 'js-profiling',
},
],
},
];
},
reactStrictMode: false,
transpilePackages: ['crypto-hash'],
// Enable production sourcemaps for Sentry
productionBrowserSourceMaps: true,
// Custom webpack config to ensure sourcemaps are generated properly
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
// Enable sourcemaps for both client and server in production
if (!dev) {
config.devtool = isServer ? 'source-map' : 'hidden-source-map';
}
return config;
},
images: {
remotePatterns: [
{
protocol: 'http',
hostname: '**',
},
{
protocol: 'https',
hostname: '**',
},
],
},
async redirects() {
return [
{
@ -76,18 +67,18 @@ export default withSentryConfig(nextConfig, {
disable: false,
// More comprehensive asset patterns for monorepo
assets: [
".next/static/**/*.js",
".next/static/**/*.js.map",
".next/server/**/*.js",
".next/server/**/*.js.map",
'.next/static/**/*.js',
'.next/static/**/*.js.map',
'.next/server/**/*.js',
'.next/server/**/*.js.map',
],
ignore: [
"**/node_modules/**",
"**/*hot-update*",
"**/_buildManifest.js",
"**/_ssgManifest.js",
"**/*.test.js",
"**/*.spec.js",
'**/node_modules/**',
'**/*hot-update*',
'**/_buildManifest.js',
'**/_ssgManifest.js',
'**/*.test.js',
'**/*.spec.js',
],
deleteSourcemapsAfterUpload: true,
},
@ -97,7 +88,8 @@ export default withSentryConfig(nextConfig, {
create: true,
finalize: true,
// Use git commit hash for releases in monorepo
name: process.env.VERCEL_GIT_COMMIT_SHA || process.env.GITHUB_SHA || undefined,
name:
process.env.VERCEL_GIT_COMMIT_SHA || process.env.GITHUB_SHA || undefined,
},
// NextJS specific optimizations for monorepo
@ -110,8 +102,10 @@ export default withSentryConfig(nextConfig, {
// Error handling for CI/CD
errorHandler: (error) => {
console.warn("Sentry build error occurred:", error.message);
console.warn("This might be due to missing Sentry environment variables or network issues");
console.warn('Sentry build error occurred:', error.message);
console.warn(
'This might be due to missing Sentry environment variables or network issues'
);
// Don't fail the build if Sentry upload fails in monorepo context
return;
},

View file

@ -5,6 +5,8 @@
"type": "module",
"scripts": {
"dev": "dotenv -e ../../.env -- next dev -p 4200",
"fetch-gtm": "node scripts/fetch-gtm.mjs",
"postinstall": "node scripts/fetch-gtm.mjs",
"build": "next build",
"build:sentry": "dotenv -e ../../.env -- next build",
"start": "dotenv -e ../../.env -- next start -p 4200",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -0,0 +1,50 @@
import { writeFile, mkdir, readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const envPath = resolve(__dirname, '..', '..', '..', '.env');
const outPath = resolve(__dirname, '..', 'public', 'g.js');
if (!process.env.NEXT_PUBLIC_GTM_ID && existsSync(envPath)) {
const content = await readFile(envPath, 'utf8');
for (const raw of content.split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const eq = line.indexOf('=');
if (eq === -1) continue;
const key = line.slice(0, eq).trim();
let value = line.slice(eq + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (!process.env[key]) process.env[key] = value;
}
}
const id = process.env.NEXT_PUBLIC_GTM_ID;
if (!id) {
console.log('[fetch-gtm] NEXT_PUBLIC_GTM_ID not set, skipping');
process.exit(0);
}
const url = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(id)}`;
try {
console.log(`[fetch-gtm] fetching ${url}`);
const res = await fetch(url);
if (!res.ok) {
console.warn(`[fetch-gtm] non-OK response ${res.status}, skipping`);
process.exit(0);
}
const body = await res.text();
await mkdir(dirname(outPath), { recursive: true });
await writeFile(outPath, body, 'utf8');
console.log(`[fetch-gtm] wrote ${outPath} (${body.length} bytes)`);
} catch (err) {
console.warn(`[fetch-gtm] failed: ${err?.message || err}, skipping`);
process.exit(0);
}

View file

@ -1,8 +1,9 @@
import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
import { sanitizePostContent } from '@gitroom/helpers/utils/sanitize.post.content';
export const dynamic = 'force-dynamic';
import { Metadata } from 'next';
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
import Image from 'next/image';
import SafeImage from '@gitroom/react/helpers/safe.image';
import Link from 'next/link';
import { CommentsComponents } from '@gitroom/frontend/components/preview/comments.components';
import dayjs from 'dayjs';
@ -10,32 +11,31 @@ import utc from 'dayjs/plugin/utc';
import { VideoOrImage } from '@gitroom/react/helpers/video.or.image';
import { CopyClient } from '@gitroom/frontend/components/preview/copy.client';
import { getT } from '@gitroom/react/translation/get.translation.service.backend';
import dynamicLoad from 'next/dynamic';
const RenderPreviewDate = dynamicLoad(
() =>
import('@gitroom/frontend/components/preview/render.preview.date').then(
(mod) => mod.RenderPreviewDate
),
{ ssr: false }
);
import { RenderPreviewDateClient } from '@gitroom/frontend/components/preview/render.preview.date.client';
import { CreationMethodBadge } from '@gitroom/frontend/components/launches/creation.method.badge';
dayjs.extend(utc);
export const metadata: Metadata = {
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Preview`,
description: '',
};
export default async function Auth({
params: { id },
searchParams,
}: {
params: {
id: string;
};
searchParams?: {
share?: string;
};
}) {
export default async function Auth(
props: {
params: Promise<{
id: string;
}>;
searchParams?: Promise<{
share?: string;
}>;
}
) {
const searchParams = await props.searchParams;
const params = await props.params;
const {
id
} = params;
const post = await (await internalFetch(`/public/posts/${id}`)).json();
const t = await getT();
if (!post.length) {
@ -57,7 +57,7 @@ export default async function Auth({
className="text-2xl flex items-center justify-center gap-[10px] text-textColor order-1"
>
<div className="max-w-[55px]">
<Image
<SafeImage
src={'/postiz.svg'}
width={55}
height={55}
@ -102,7 +102,7 @@ export default async function Auth({
)}
<div className="flex-1">
{t('publication_date', 'Publication Date:')}{' '}
<RenderPreviewDate date={post[0].publishDate} />
<RenderPreviewDateClient date={post[0].publishDate} />
</div>
</div>
</div>
@ -143,12 +143,18 @@ export default async function Auth({
<span className="text-sm text-gray-500">
@{post[0].integration.profile}
</span>
{index === 0 && (
<CreationMethodBadge
creationMethod={p.creationMethod}
size="md"
/>
)}
</div>
<div className="flex flex-col gap-[20px]">
<div
className="text-sm whitespace-pre-wrap"
dangerouslySetInnerHTML={{
__html: p.content,
__html: sanitizePostContent(p.content),
}}
/>
<div className="flex w-full gap-[10px]">

View file

@ -0,0 +1,17 @@
export const dynamic = 'force-dynamic';
import { AdminErrorsComponent } from '@gitroom/frontend/components/admin/admin-errors.component';
import { Metadata } from 'next';
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
export const metadata: Metadata = {
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Admin Errors`,
description: '',
};
export default async function Page() {
return (
<div className="bg-newBgColorInner flex-1 flex-col flex p-[20px] gap-[12px]">
<AdminErrorsComponent />
</div>
);
}

View file

@ -6,12 +6,11 @@ export const metadata: Metadata = {
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Settings`,
description: '',
};
export default async function Index({
searchParams,
}: {
searchParams: {
export default async function Index(props: {
searchParams: Promise<{
code: string;
};
}>;
}) {
const searchParams = await props.searchParams;
return <SettingsPopup />;
}

View file

@ -19,16 +19,17 @@ function iteratorToStream(iterator: any) {
},
});
}
export const GET = (
export const GET = async (
request: NextRequest,
context: {
params: {
path: string[];
};
params: Promise<{
path?: string[];
}>;
}
) => {
const { path } = await context.params;
const filePath =
process.env.UPLOAD_DIRECTORY + '/' + context.params.path.join('/');
process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');
const response = createReadStream(filePath);
const fileStats = statSync(filePath);
const contentType = mime.getType(filePath) || 'application/octet-stream';

View file

@ -7,9 +7,9 @@ export const metadata: Metadata = {
description: '',
};
export default async function Auth(params: {
params: {
params: Promise<{
token: string;
};
}>;
}) {
return <ForgotReturn token={params.params.token} />;
return <ForgotReturn token={(await params.params).token} />;
}

View file

@ -2,7 +2,6 @@ import { getT } from '@gitroom/react/translation/get.translation.service.backend
export const dynamic = 'force-dynamic';
import { ReactNode } from 'react';
import Image from 'next/image';
import loadDynamic from 'next/dynamic';
import { TestimonialComponent } from '@gitroom/frontend/components/auth/testimonial.component';
import { LogoTextComponent } from '@gitroom/frontend/components/ui/logo-text.component';

View file

@ -10,13 +10,13 @@ export const metadata: Metadata = {
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Register`,
description: '',
};
export default async function Auth(params: {searchParams: {provider: string}}) {
export default async function Auth(params: {searchParams: Promise<{provider: string}>}) {
const t = await getT();
if (process.env.DISABLE_REGISTRATION === 'true') {
const canRegister = (
await (await internalFetch('/auth/can-register')).json()
).register;
if (!canRegister && !params?.searchParams?.provider) {
if (!canRegister && !(await params?.searchParams)?.provider) {
return (
<>
<LoginWithOidc />

View file

@ -3,15 +3,21 @@ import { cookies } from 'next/headers';
export const dynamic = 'force-dynamic';
export default async function Page({
params: { provider },
searchParams,
}: {
params: {
provider: string;
};
searchParams: any;
}) {
const get = cookies().get('auth');
export default async function Page(
props: {
params: Promise<{
provider: string;
}>;
searchParams: Promise<any>;
}
) {
const searchParams = await props.searchParams;
const params = await props.params;
const {
provider
} = params;
const get = (await cookies()).get('auth');
return <ContinueIntegration searchParams={searchParams} provider={provider} logged={!!get?.name} />;
}

View file

@ -15,17 +15,15 @@ import { PHProvider } from '@gitroom/react/helpers/posthog';
import UtmSaver from '@gitroom/helpers/utils/utm.saver';
import { DubAnalytics } from '@gitroom/frontend/components/layout/dubAnalytics';
import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.component';
import { headers } from 'next/headers';
import { headerName } from '@gitroom/react/translation/i18n.config';
import { GoogleTagManagerComponent } from '@gitroom/frontend/components/layout/gtm.component';
import { cookies } from 'next/headers';
import {
cookieName,
fallbackLng,
} from '@gitroom/react/translation/i18n.config';
import { HtmlComponent } from '@gitroom/frontend/components/layout/html.component';
import Script from 'next/script';
// import dynamicLoad from 'next/dynamic';
// const SetTimezone = dynamicLoad(
// () => import('@gitroom/frontend/components/layout/set.timezone'),
// {
// ssr: false,
// }
// );
import { ChangeDirClient } from '@gitroom/frontend/components/new-layout/change.dir.client';
const jakartaSans = Plus_Jakarta_Sans({
weight: ['600', '500'],
@ -34,7 +32,8 @@ const jakartaSans = Plus_Jakarta_Sans({
});
export default async function AppLayout({ children }: { children: ReactNode }) {
const allHeaders = headers();
const cookieStore = await cookies();
const language = cookieStore.get(cookieName)?.value || fallbackLng;
const Plausible = !!process.env.STRIPE_PUBLISHABLE_KEY
? PlausibleProvider
: Fragment;
@ -51,6 +50,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
/>
)}
</head>
<ChangeDirClient />
<body
className={clsx(jakartaSans.className, 'dark text-primary !bg-primary')}
>
@ -70,6 +70,8 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
oauthLogoUrl={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL!}
oauthDisplayName={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME!}
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
cloudflareUrl={process.env.CLOUDFLARE_BUCKET_URL || ''}
mainUrl={process.env.MAIN_URL || ''}
mcpUrl={process.env.MCP_URL}
dub={!!process.env.STRIPE_PUBLISHABLE_KEY}
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
@ -80,7 +82,9 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS}
sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!}
extensionId={process.env.EXTENSION_ID || ''}
language={allHeaders.get(headerName)}
googleAdsId={process.env.NEXT_PUBLIC_GTM_ID}
googleAdsTrialTracking={process.env.NEXT_PUBLIC_TRACKING_TRIAL}
language={language}
transloadit={
process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE
? [
@ -95,6 +99,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
<HtmlComponent />
<DubAnalytics />
<FacebookComponent />
<GoogleTagManagerComponent gtmId={process.env.NEXT_PUBLIC_GTM_ID} />
<Plausible
domain={!!process.env.IS_GENERAL ? 'postiz.com' : 'gitroom.com'}
>

View file

@ -41,6 +41,8 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
oauthLogoUrl={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL!}
oauthDisplayName={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME!}
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
cloudflareUrl={process.env.CLOUDFLARE_BUCKET_URL || ''}
mainUrl={process.env.MAIN_URL || ''}
mcpUrl={process.env.MCP_URL}
dub={false}
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}

View file

@ -1,7 +1,7 @@
'use client';
import { StandaloneModal } from '@gitroom/frontend/components/standalone-modal/standalone.modal';
export default async function Modal() {
export default function Modal() {
return (
<div className="w-screen h-screen overflow-hidden bg-black">
<div className="text-textColor h-[calc(100vh+80px)] w-[calc(100vw+80px)] -m-[40px]">

View file

@ -0,0 +1,77 @@
import { MantineWrapper } from '@gitroom/react/helpers/mantine.wrapper';
export const dynamic = 'force-dynamic';
import '../global.scss';
import 'react-tooltip/dist/react-tooltip.css';
import '@copilotkit/react-ui/styles.css';
import LayoutContext from '@gitroom/frontend/components/layout/layout.context';
import { ReactNode } from 'react';
import { Plus_Jakarta_Sans } from 'next/font/google';
import clsx from 'clsx';
import { VariableContextComponent } from '@gitroom/react/helpers/variable.context';
import UtmSaver from '@gitroom/helpers/utils/utm.saver';
const jakartaSans = Plus_Jakarta_Sans({
weight: ['600', '500'],
style: ['normal', 'italic'],
subsets: ['latin'],
});
export default async function AppLayout({ children }: { children: ReactNode }) {
return (
<html>
<head>
<link rel="icon" href="/favicon.ico" sizes="any" />
</head>
<body
className={clsx(jakartaSans.className, 'dark text-primary !bg-primary')}
>
<VariableContextComponent
language="en"
storageProvider={
process.env.STORAGE_PROVIDER! as 'local' | 'cloudflare'
}
stripeClient=""
environment={process.env.NODE_ENV!}
backendUrl={process.env.NEXT_PUBLIC_BACKEND_URL!}
plontoKey={process.env.NEXT_PUBLIC_POLOTNO!}
billingEnabled={!!process.env.STRIPE_PUBLISHABLE_KEY}
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
frontEndUrl={process.env.FRONTEND_URL!}
isGeneral={!!process.env.IS_GENERAL}
genericOauth={!!process.env.POSTIZ_GENERIC_OAUTH}
oauthLogoUrl={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL!}
oauthDisplayName={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME!}
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
cloudflareUrl={process.env.CLOUDFLARE_BUCKET_URL || ''}
mainUrl={process.env.MAIN_URL || ''}
mcpUrl={process.env.MCP_URL}
dub={false}
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
telegramBotName={process.env.TELEGRAM_BOT_NAME!}
neynarClientId={process.env.NEYNAR_CLIENT_ID!}
isSecured={!process.env.NOT_SECURED}
disableImageCompression={!!process.env.DISABLE_IMAGE_COMPRESSION}
disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS}
sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!}
extensionId={process.env.EXTENSION_ID || ''}
transloadit={
process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE
? [
process.env.TRANSLOADIT_AUTH!,
process.env.TRANSLOADIT_TEMPLATE!,
]
: []
}
>
<MantineWrapper>
<LayoutContext>
<UtmSaver />
{children}
</LayoutContext>
</MantineWrapper>
</VariableContextComponent>
</body>
</html>
);
}

View file

@ -0,0 +1,94 @@
'use client';
import { FC, useEffect, useRef, useState } from 'react';
import {
ProviderPreviewComponent,
type ProviderPreviewHandle,
type ProviderPreviewProps,
type ProviderPreviewValidation,
} from '@gitroom/frontend/components/provider-preview/preview.provider.component';
type InitPayload = {
value?: Record<string, unknown>;
errors?: string[];
integration?: ProviderPreviewProps['integration'];
/**
* Per-post media (outer array = thread entries, inner = media items).
* Passed to the provider's `checkValidity` function during validation.
*/
posts?: Array<Array<{ path: string; thumbnail?: string }>>;
};
declare global {
interface Window {
__PROVIDER_INIT__?: InitPayload;
__getProviderPreviewValues__?: () => Record<string, unknown>;
__validateProviderPreview__?: () => Promise<ProviderPreviewValidation>;
/**
* Returns the provider's resolved character limit (number) or null when
* the provider doesn't declare one. Resolution uses the seeded
* __PROVIDER_INIT__.integration.additionalSettings (e.g. X bumps to
* 4000 when {title:'Verified', value:true} is present).
*/
__getProviderMaxCharacters__?: () => number | null;
}
}
const ProviderPreviewBridge: FC<{ provider: string }> = ({
provider,
}) => {
// Read __PROVIDER_INIT__ in an effect, not via a useState lazy
// initializer. The initializer would run on the server (where `window`
// is undefined → {}), and during hydration React reuses the server
// state — so the seeded payload would never reach the form. Setting
// state inside an effect guarantees the read happens client-side
// after mount; useForm's `values` prop then reactively resets the
// form to the seed AFTER any field-level `register('x', { value })`
// defaults have been applied, so the seed wins.
const [init, setInit] = useState<InitPayload>(null);
useEffect(() => {
if (typeof window !== 'undefined' && window.__PROVIDER_INIT__) {
setInit(window.__PROVIDER_INIT__ || {});
}
}, []);
const controlRef = useRef<ProviderPreviewHandle | null>(null);
useEffect(() => {
window.__getProviderPreviewValues__ = () =>
controlRef.current?.getValues() ?? {};
window.__validateProviderPreview__ = async () =>
controlRef.current
? await controlRef.current.validate()
: {
isValid: false,
value: {},
errors: ['not-ready'],
formValid: false,
checkValidityError: null,
};
window.__getProviderMaxCharacters__ = () =>
controlRef.current?.getMaximumCharacters() ?? null;
return () => {
delete window.__getProviderPreviewValues__;
delete window.__validateProviderPreview__;
delete window.__getProviderMaxCharacters__;
};
}, []);
if (!init) {
return null;
}
return (
<ProviderPreviewComponent
provider={provider}
value={init.value}
errors={init.errors}
integration={init.integration}
posts={init.posts}
controlRef={controlRef}
/>
);
};
export default ProviderPreviewBridge;

View file

@ -0,0 +1,13 @@
'use client';
import dynamic from 'next/dynamic';
import { FC } from 'react';
const Bridge = dynamic(
() =>
import(
'./bridge'
).then((mod) => mod.default),
{ ssr: false }
);
export const InBridge: FC<{ provider: string }> = ({ provider }) => {
return <Bridge provider={provider} />;
};

View file

@ -0,0 +1,67 @@
/**
* Provider settings WebView bridge.
*
* URL: /provider/:p (e.g. /provider/tiktok, /provider/instagram)
*
* --- Auth (native -> WebView, via URL) ---
* Append `?loggedAuth=<jwt>` to the URL. The shared fetch wrapper
* (libraries/helpers/src/utils/custom.fetch.func.ts) reads that search
* param on every request and attaches it as the `auth` header, so any
* authenticated API call made by the SettingsComponent or checkValidity
* just works. The (provider) route is also excluded from the 401->/
* redirect logic in LayoutContext, so a stale token won't yank the
* WebView away from the form.
*
* --- Initial state (native -> WebView, push once) ---
* Before loading the URL, the native side injects a global:
*
* webView.injectJavaScript(`window.__PROVIDER_INIT__ = ${JSON.stringify({
* value: { ...currentSettings }, // optional, shape = provider DTO
* errors: ['...'], // optional, prior validation errors
* integration: { ... }, // optional Partial<Integration>
* })};`);
*
* The bridge reads this once on mount (see ./bridge.tsx).
*
* --- Reading values & validation (native -> WebView, pull on demand) ---
* No messages are posted from the WebView. Instead, native calls these
* globals (they are defined once the bridge's effect has run):
*
* // Returns the current form values, no validation:
* webView.evaluateJavaScript('window.__getProviderPreviewValues__()')
* // => { ...settings }
*
* // Triggers validation and returns isValid + flattened error strings:
* webView.evaluateJavaScript('window.__validateProviderPreview__()')
* // => Promise<{ isValid: boolean, value: {...}, errors: string[] }>
*
* // Returns the provider's resolved character limit (number) or null
* // when the provider doesn't declare one. Uses the seeded
* // __PROVIDER_INIT__.integration.additionalSettings:
* webView.evaluateJavaScript('window.__getProviderMaxCharacters__()')
* // => number | null
*
* React Native example (RN WebView ref):
* const js = `window.__validateProviderPreview__().then(r =>
* window.ReactNativeWebView.postMessage(JSON.stringify(r)));
* true;`;
* webViewRef.current?.injectJavaScript(js);
*
* Native should wait for page load (onLoadEnd / didFinishNavigation) before
* calling these. If called before the bridge mounts, the validate getter
* returns { isValid: false, errors: ['not-ready'] } and the values getter
* returns {}.
*
* If a different channel is needed, adjust ./bridge.tsx this page is only
* a server wrapper that forwards the `:p` route param.
*/
import { InBridge } from '@gitroom/frontend/app/(provider)/provider/[p]/in-bridge';
export default async function Page({
params,
}: {
params: Promise<{ p: string }>;
}) {
const { p } = await params;
return <InBridge provider={p} />;
}

View file

@ -0,0 +1,6 @@
import React from 'react';
import { MobileIntegration } from '@gitroom/frontend/components/new-layout/mobile.integration';
export default async function Page() {
return <MobileIntegration />;
}

View file

@ -1,10 +1,15 @@
@use './colors.scss';
@use './polonto.css';
@use '@uppy/core/dist/style.css' as uppyCore;
@use '@uppy/dashboard/dist/style.css' as uppyDashboard;
@tailwind base;
@tailwind components;
@tailwind utilities;
@import './colors.scss';
@import './polonto.css';
@import '@uppy/core/dist/style.css';
@import '@uppy/dashboard/dist/style.css';
@keyframes spin {
to { transform: rotate(360deg); }
}
body {
background: var(--new-bgColor) !important;

View file

@ -0,0 +1,411 @@
'use client';
import React, { FC, useCallback, useMemo, useState } from 'react';
import useSWR from 'swr';
import copy from 'copy-to-clipboard';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
import { Button } from '@gitroom/react/form/button';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
interface ErrorRow {
id: string;
message: string;
body: string;
platform: string;
postId: string;
createdAt: string;
organization: {
id: string;
name: string;
users: { user: { id: string; email: string; name: string | null } }[];
};
post: { id: string; content: string | null };
}
interface ErrorsResponse {
items: ErrorRow[];
total: number;
page: number;
limit: number;
hasMore: boolean;
}
const safeParse = (value: string) => {
try {
return JSON.parse(value);
} catch {
return value;
}
};
const ErrorDetailsModal: FC<{ row: ErrorRow }> = ({ row }) => {
const modal = useModals();
const toaster = useToaster();
const parsedMessage = useMemo(() => safeParse(row.message), [row.message]);
const parsedBody = useMemo(() => safeParse(row.body), [row.body]);
const copyAll = useCallback(() => {
copy(
JSON.stringify(
{ message: parsedMessage, body: parsedBody, meta: row },
null,
2
)
);
toaster.show('Debug code copied to clipboard', 'success');
}, [parsedMessage, parsedBody, row, toaster]);
return (
<div className="rounded-[4px] border border-newTableBorder bg-newBgColorInner px-[16px] pb-[16px] relative w-full max-h-[80vh] overflow-auto">
<div className="sticky top-0 bg-newBgColorInner py-[16px] flex items-center justify-between gap-[12px] z-10 border-b border-newTableBorder mb-[12px]">
<div className="text-[16px] font-[600]">Error Details</div>
<div className="flex gap-[8px] items-center">
<Button onClick={copyAll}>Copy Debug Code</Button>
<button
className="outline-none w-[28px] h-[28px] flex items-center justify-center hover:bg-tableBorder cursor-pointer rounded"
type="button"
onClick={() => modal.closeAll()}
>
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
>
<path
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-[12px] text-[13px] mb-[12px]">
<div>
<div className="opacity-60">Platform</div>
<div>{row.platform}</div>
</div>
<div>
<div className="opacity-60">Created</div>
<div>{new Date(row.createdAt).toLocaleString()}</div>
</div>
<div>
<div className="opacity-60">Organization</div>
<div>
{row.organization?.name}{' '}
<span className="opacity-60">({row.organization?.id})</span>
</div>
</div>
<div>
<div className="opacity-60">Users</div>
<div className="break-all">
{row.organization?.users
?.map((u) => u.user?.email)
.filter(Boolean)
.join(', ') || '—'}
</div>
</div>
<div className="col-span-2">
<div className="opacity-60">Post ID</div>
<div>{row.postId}</div>
</div>
</div>
<div className="text-[13px] font-[600] mb-[6px]">message</div>
<pre className="text-[12px] bg-sixth p-[12px] rounded overflow-auto max-h-[40vh] whitespace-pre-wrap break-all">
{typeof parsedMessage === 'string'
? parsedMessage
: JSON.stringify(parsedMessage, null, 2)}
</pre>
<div className="text-[13px] font-[600] mb-[6px] mt-[12px]">body</div>
<pre className="text-[12px] bg-sixth p-[12px] rounded overflow-auto max-h-[40vh] whitespace-pre-wrap break-all">
{typeof parsedBody === 'string'
? parsedBody
: JSON.stringify(parsedBody, null, 2)}
</pre>
</div>
);
};
const usePlatformsList = () => {
const fetch = useFetch();
return useSWR<string[]>('/admin/errors/platforms', async (url: string) => {
const res = await fetch(url);
if (!res.ok) return [];
return res.json();
});
};
const useErrorsList = (params: {
page: number;
limit: number;
platform: string;
email: string;
unknownFirst: boolean;
}) => {
const fetch = useFetch();
const query = new URLSearchParams({
page: String(params.page),
limit: String(params.limit),
...(params.platform ? { platform: params.platform } : {}),
...(params.email ? { email: params.email } : {}),
unknownFirst: params.unknownFirst ? 'true' : 'false',
});
const key = `/admin/errors?${query.toString()}`;
return useSWR<ErrorsResponse>(key, async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
throw new Error('Failed to load errors');
}
return res.json();
});
};
export const AdminErrorsComponent: FC = () => {
const user = useUser();
const modal = useModals();
const toaster = useToaster();
const [page, setPage] = useState(0);
const [limit, setLimit] = useState(20);
const [platform, setPlatform] = useState('');
const [email, setEmail] = useState('');
const [emailInput, setEmailInput] = useState('');
const [unknownFirst, setUnknownFirst] = useState(true);
const { data: platforms } = usePlatformsList();
const { data, isLoading, error } = useErrorsList({
page,
limit,
platform,
email,
unknownFirst,
});
const onApplyEmail = useCallback(() => {
setPage(0);
setEmail(emailInput.trim());
}, [emailInput]);
const onClear = useCallback(() => {
setPage(0);
setEmail('');
setEmailInput('');
setPlatform('');
}, []);
const openDetails = useCallback(
(row: ErrorRow) => {
modal.openModal({
closeOnClickOutside: true,
withCloseButton: false,
classNames: {
modal: 'w-[100%] max-w-[1100px] text-textColor',
},
children: <ErrorDetailsModal row={row} />,
});
},
[modal]
);
const copyRow = useCallback(
(row: ErrorRow) => {
copy(
JSON.stringify(
{ message: safeParse(row.message), body: safeParse(row.body), meta: row },
null,
2
)
);
toaster.show('Debug code copied to clipboard', 'success');
},
[toaster]
);
if (!user?.isSuperAdmin) {
return (
<div className="text-textColor p-[20px]">
You do not have access to this page.
</div>
);
}
const totalPages = data ? Math.max(1, Math.ceil(data.total / limit)) : 1;
return (
<div className="flex flex-col gap-[16px] text-textColor">
<div className="flex items-center justify-between">
<div className="text-[20px] font-[600]">Errors</div>
<div className="text-[13px] opacity-70">
{data ? `${data.total} total` : ''}
</div>
</div>
<div className="flex flex-wrap gap-[12px] items-end bg-newBgColorInner border border-newTableBorder rounded-[8px] p-[12px]">
<div className="flex flex-col gap-[6px]">
<div className="text-[12px] opacity-70">Platform</div>
<select
value={platform}
onChange={(e) => {
setPage(0);
setPlatform(e.target.value);
}}
className="bg-newBgColorInner h-[38px] border border-newTableBorder rounded-[8px] px-[10px] text-[14px] text-textColor min-w-[180px]"
>
<option value="">All platforms</option>
{(platforms || []).map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
<div className="flex flex-col gap-[6px]">
<div className="text-[12px] opacity-70">Email contains</div>
<div className="flex gap-[8px]">
<input
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') onApplyEmail();
}}
placeholder="user@example.com"
className="bg-newBgColorInner h-[38px] border border-newTableBorder rounded-[8px] px-[10px] text-[14px] text-textColor min-w-[240px]"
/>
<Button onClick={onApplyEmail}>Apply</Button>
</div>
</div>
<label className="flex items-center gap-[6px] text-[13px] cursor-pointer h-[38px]">
<input
type="checkbox"
checked={unknownFirst}
onChange={(e) => {
setPage(0);
setUnknownFirst(e.target.checked);
}}
/>
Unknown Error first
</label>
<div className="flex flex-col gap-[6px]">
<div className="text-[12px] opacity-70">Per page</div>
<select
value={limit}
onChange={(e) => {
setPage(0);
setLimit(parseInt(e.target.value, 10));
}}
className="bg-newBgColorInner h-[38px] border border-newTableBorder rounded-[8px] px-[10px] text-[14px] text-textColor"
>
{[10, 20, 50, 100].map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</div>
<Button secondary onClick={onClear}>
Clear filters
</Button>
</div>
{isLoading ? (
<LoadingComponent />
) : error ? (
<div className="text-red-400">Failed to load errors.</div>
) : !data || data.items.length === 0 ? (
<div className="opacity-70">No errors found.</div>
) : (
<div className="border border-newTableBorder rounded-[8px] overflow-hidden">
<div className="grid grid-cols-[170px_120px_220px_1fr_220px] gap-[12px] px-[12px] py-[10px] bg-newBgColorInner text-[12px] uppercase opacity-70 border-b border-newTableBorder">
<div>Created</div>
<div>Platform</div>
<div>User / Org</div>
<div>Message</div>
<div className="text-right">Actions</div>
</div>
{data.items.map((row) => {
const isUnknown = (row.message || '').includes('Unknown Error');
const emails =
row.organization?.users
?.map((u) => u.user?.email)
.filter(Boolean)
.join(', ') || '—';
const preview =
(row.message || '').length > 280
? row.message.slice(0, 280) + '…'
: row.message;
return (
<div
key={row.id}
className="grid grid-cols-[170px_120px_220px_1fr_220px] gap-[12px] px-[12px] py-[10px] text-[13px] border-b border-newTableBorder last:border-b-0 items-start"
>
<div className="opacity-90">
{new Date(row.createdAt).toLocaleString()}
</div>
<div>
<span
className={
isUnknown
? 'text-red-400 font-[600]'
: 'opacity-90'
}
>
{row.platform}
</span>
</div>
<div className="break-all">
<div>{emails}</div>
<div className="opacity-60 text-[12px]">
{row.organization?.name}
</div>
</div>
<div className="break-all whitespace-pre-wrap font-mono text-[12px] opacity-90">
{preview}
</div>
<div className="flex gap-[8px] justify-end">
<Button secondary onClick={() => openDetails(row)}>
View
</Button>
<Button onClick={() => copyRow(row)}>Copy</Button>
</div>
</div>
);
})}
</div>
)}
<div className="flex items-center justify-between">
<div className="text-[13px] opacity-70">
Page {page + 1} of {totalPages}
</div>
<div className="flex gap-[8px]">
<Button
secondary
disabled={page === 0}
onClick={() => setPage((p) => Math.max(0, p - 1))}
>
Previous
</Button>
<Button
disabled={!data?.hasMore}
onClick={() => setPage((p) => p + 1)}
>
Next
</Button>
</div>
</div>
</div>
);
};

View file

@ -33,6 +33,7 @@ import dayjs from 'dayjs';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { hasExtension } from '@gitroom/helpers/utils/has.extension';
export const AgentChat: FC = () => {
const { backendUrl } = useVariables();
@ -93,10 +94,11 @@ const LoadMessages: FC<{ id: string }> = ({ id }) => {
const loadMessages = useCallback(async (idToSet: string) => {
const data = await (await fetch(`/copilot/${idToSet}/list`)).json();
console.log(data);
setMessages(
data.uiMessages.map((p: any) => {
data.messages.map((p: any) => {
return new TextMessage({
content: p.content,
content: p.content.content,
role: p.role,
});
})
@ -161,7 +163,7 @@ const NewInput: FC<InputProps> = (props) => {
? '\n[--Media--]' +
media
.map((m) =>
m.path.indexOf('mp4') > -1
hasExtension(m.path, 'mp4')
? `Video: ${m.path}`
: `Image: ${m.path}`
)

View file

@ -14,7 +14,7 @@ import useSWR from 'swr';
import { orderBy } from 'lodash';
import { SVGLine } from '@gitroom/frontend/components/launches/launches.component';
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
import Image from 'next/image';
import SafeImage from '@gitroom/react/helpers/safe.image';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useWaitForClass } from '@gitroom/helpers/utils/use.wait.for.class';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
@ -172,7 +172,7 @@ export const AgentList: FC<{ onChange: (arr: any[]) => void }> = ({
width={36}
height={36}
/>
<Image
<SafeImage
src={`/icons/platforms/${integration.identifier}.png`}
className="rounded-[8px] absolute z-10 bottom-[5px] -end-[5px] border border-fifth"
alt={integration.identifier}

View file

@ -16,7 +16,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import clsx from 'clsx';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import ReactLoading from 'react-loading';
import ReactLoading from '@gitroom/frontend/components/layout/loading';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const UpDown: FC<{

View file

@ -1,7 +1,7 @@
'use client';
import { useCallback } from 'react';
import Image from 'next/image';
import SafeImage from '@gitroom/react/helpers/safe.image';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
@ -29,7 +29,7 @@ export const OauthProvider = () => {
className={`cursor-pointer flex-1 bg-white h-[44px] rounded-[4px] flex justify-center items-center text-customColor16 gap-[4px]`}
>
<div>
<Image
<SafeImage
src={oauthLogoUrl || '/icons/generic-oauth.svg'}
alt="genericOauth"
width={40}

View file

@ -1,5 +1,5 @@
import { FC } from 'react';
import Image from 'next/image';
import SafeImage from '@gitroom/react/helpers/safe.image';
export const Testimonial: FC<{
picture: string;
@ -12,7 +12,7 @@ export const Testimonial: FC<{
{/* Header */}
<div className="flex gap-[12px] min-w-0">
<div className="w-[36px] h-[36px] rounded-full overflow-hidden shrink-0">
<Image src={picture} alt={name} width={36} height={36} />
<SafeImage src={picture} alt={name} width={36} height={36} />
</div>
<div className="flex flex-col -mt-[4px] min-w-0">

Some files were not shown because too many files have changed in this diff Show more