dunsocial 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/index.js +146 -0
- package/package.json +55 -0
- package/skills/dunsocial/SKILL.md +134 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) DunSocial / THISUX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# DunSocial CLI (`dun`)
|
|
2
|
+
|
|
3
|
+
Agent-first command line client for [DunSocial](https://dunsocial.com).
|
|
4
|
+
|
|
5
|
+
Wraps the existing Mobile API (`apps/api`) — **no duplicate publish logic**.
|
|
6
|
+
Remote AI hosts (ChatGPT / Claude web) should keep using **MCP**. This CLI is for local agents (Claude Code, Codex), CI, and power users.
|
|
7
|
+
|
|
8
|
+
Linear: [DUN-262](https://linear.app/thisuxco/issue/DUN-262) · [DUN-263](https://linear.app/thisuxco/issue/DUN-263) · [DUN-264](https://linear.app/thisuxco/issue/DUN-264)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
### Monorepo (dev)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun install
|
|
16
|
+
bun run dev:cli -- --help
|
|
17
|
+
bun apps/cli/src/index.ts auth status --json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Built package (Node 20+)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun run --cwd apps/cli build
|
|
24
|
+
node apps/cli/dist/index.js --help
|
|
25
|
+
|
|
26
|
+
# npm (when published)
|
|
27
|
+
npm i -g dunsocial
|
|
28
|
+
dun --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Bins: `dun` · `dunsocial`. Package name: `dunsocial`.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 1. Authenticate with a Better Auth bearer session token
|
|
37
|
+
export DUN_TOKEN=... # or: dun auth login --token ...
|
|
38
|
+
export DUN_API_URL=https://api.dunsocial.com # optional
|
|
39
|
+
|
|
40
|
+
# 2. Pick a workspace
|
|
41
|
+
dun workspace list --json
|
|
42
|
+
dun workspace use <id-or-slug>
|
|
43
|
+
|
|
44
|
+
# 3. Discover connected accounts
|
|
45
|
+
dun accounts list --json
|
|
46
|
+
|
|
47
|
+
# 4. Schedule
|
|
48
|
+
dun posts schedule \
|
|
49
|
+
--text "Hello from the DunSocial CLI" \
|
|
50
|
+
--accounts <socialAccountId> \
|
|
51
|
+
--in 1h \
|
|
52
|
+
--json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Auth
|
|
56
|
+
|
|
57
|
+
### Device login (default)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
dun auth login
|
|
61
|
+
# opens app.dunsocial.com/cli/authorize — approve, CLI stores token
|
|
62
|
+
dun auth login --no-browser # print URL only
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### PAT / CI (recommended for automation)
|
|
66
|
+
|
|
67
|
+
Create a token in **Settings → CLI** (hashed at rest, scoped, revocable):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export DUN_TOKEN='dun_pat_…'
|
|
71
|
+
export DUN_WORKSPACE_ID='…'
|
|
72
|
+
dun auth status --json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Session token (humans / legacy)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dun auth login --token <bearer-session>
|
|
79
|
+
export DUN_TOKEN=...
|
|
80
|
+
dun auth whoami --json
|
|
81
|
+
dun auth logout
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Config: `~/.config/dunsocial/config.json` (0600). Override path with `DUN_CONFIG`.
|
|
85
|
+
|
|
86
|
+
Env (wins over config):
|
|
87
|
+
|
|
88
|
+
| Var | Purpose |
|
|
89
|
+
|-----|---------|
|
|
90
|
+
| `DUN_TOKEN` | Bearer token |
|
|
91
|
+
| `DUN_WORKSPACE_ID` | Default workspace |
|
|
92
|
+
| `DUN_API_URL` | API base URL |
|
|
93
|
+
| `DUN_JSON=1` | Force JSON output |
|
|
94
|
+
| `DUN_DEBUG=1` | Log HTTP to stderr |
|
|
95
|
+
|
|
96
|
+
PATs with scoped UI management are a later follow-up. See `docs/cli.md` for a GitHub Actions announce example.
|
|
97
|
+
|
|
98
|
+
## Commands
|
|
99
|
+
|
|
100
|
+
See `dun --help` for the full map. Highlights:
|
|
101
|
+
|
|
102
|
+
| Area | Examples |
|
|
103
|
+
|------|----------|
|
|
104
|
+
| Workspaces | `workspace list\|use\|current` |
|
|
105
|
+
| Accounts | `accounts list [--platform x]` |
|
|
106
|
+
| Posts | `schedule`, `publish`, `list`, `get`, `reschedule`, `cancel`, `delete`, `x-cap`, `schedule-thread`, `publish-thread` |
|
|
107
|
+
| Drafts | `create`, `list`, `get`, `update`, `delete` |
|
|
108
|
+
| Media | `upload <file>`, `list`, `get`, `delete` |
|
|
109
|
+
| Memory | `collections list\|create`, `save`, `list`, `search`, `delete` |
|
|
110
|
+
| Skills | `skill install claude\|codex` |
|
|
111
|
+
|
|
112
|
+
### Global flags
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
--json --workspace <id> --api-url <url> --token <token>
|
|
116
|
+
--debug --quiet --yes --help --version
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
JSON is the default when stdout is not a TTY or `CI=1`.
|
|
120
|
+
|
|
121
|
+
### Exit codes
|
|
122
|
+
|
|
123
|
+
0 ok · 1 usage · 2 auth · 3 forbidden · 4 validation · 5 not found · 6 rate limited · 7 network
|
|
124
|
+
|
|
125
|
+
## Agent skills
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
dun skill install claude # ~/.claude/skills/dunsocial/SKILL.md
|
|
129
|
+
dun skill install codex # ~/.codex/skills/... + AGENTS.md block
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bun run --cwd apps/cli test
|
|
136
|
+
bun run --cwd apps/cli type-check
|
|
137
|
+
bun run --cwd apps/cli build
|
|
138
|
+
bun run --cwd apps/cli release-check
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Releases (npm)
|
|
142
|
+
|
|
143
|
+
Do **not** bump `version` by hand for normal releases.
|
|
144
|
+
|
|
145
|
+
1. Merge conventional commits affecting `apps/cli` to `main`
|
|
146
|
+
2. Merge the **release-please** PR (version + `CHANGELOG.md`)
|
|
147
|
+
3. CI publishes `dunsocial` to npm (`NPM_TOKEN` secret)
|
|
148
|
+
|
|
149
|
+
See [docs/release.md](../../docs/release.md). Manual: Actions → **Release CLI** → run with an existing tag.
|
|
150
|
+
|
|
151
|
+
## Architecture
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
dun → HTTPS + Bearer + X-Workspace-Id → apps/api REST
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Do not import DB/MCP server code into the CLI. Keep it a thin client.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import{mkdir as dn,readFile as Ie,writeFile as rn,chmod as tn,unlink as mn}from"node:fs/promises";import{dirname as yn,join as ue}from"node:path";import{homedir as pe}from"node:os";var o={OK:0,USAGE:1,AUTH:2,FORBIDDEN:3,VALIDATION:4,NOT_FOUND:5,RATE_LIMITED:6,NETWORK:7};class w extends Error{code;details;constructor(e,n=o.USAGE,u){super(e);this.name="CliError",this.code=n,this.details=u}}function ne(e){if(e===401)return o.AUTH;if(e===403)return o.FORBIDDEN;if(e===404)return o.NOT_FOUND;if(e===429)return o.RATE_LIMITED;if(e>=400&&e<500)return o.VALIDATION;return o.NETWORK}var Ge="https://api.dunsocial.com",W="0.2.0";function Ve(){if(process.env.DUN_CONFIG)return process.env.DUN_CONFIG;let e=process.env.XDG_CONFIG_HOME,n=e?e:ue(pe(),".config");return ue(n,"dunsocial","config.json")}async function _e(){let e=Ve();try{let n=await Ie(e,"utf8");return JSON.parse(n)}catch(n){if(n.code==="ENOENT")return{};throw new w(`Failed to read config at ${e}: ${n.message}`,o.USAGE)}}async function de(e){let n=await _e(),u=(e.apiUrl||process.env.DUN_API_URL||n.apiUrl||Ge).replace(/\/$/,""),t=e.token||process.env.DUN_TOKEN||n.token,y=e.workspace||process.env.DUN_WORKSPACE_ID||n.defaultWorkspaceId||void 0,d=process.env.DUN_JSON==="0",i=!process.stdout.isTTY,r=d?!1:e.json===!0||process.env.DUN_JSON==="1"||process.env.CI==="true"||process.env.CI==="1"||i;return{apiUrl:u,token:t,workspaceId:y,json:r,debug:e.debug===!0||process.env.DUN_DEBUG==="1",quiet:e.quiet===!0,yes:e.yes===!0||process.env.CI==="true"||process.env.CI==="1",config:n}}function re(e){let n={},u=[],t=0,y=!0;while(t<e.length){let O=e[t];if(y&&O==="--"){y=!1,t+=1;continue}if(y&&O.startsWith("--")){let A=O.indexOf("=");if(A!==-1){n[O.slice(2,A)]=O.slice(A+1),t+=1;continue}let T=O.slice(2),U=e[t+1];if(U!==void 0&&!U.startsWith("-"))n[T]=U,t+=2;else n[T]=!0,t+=1;continue}if(y&&O.startsWith("-")&&O.length>1&&O!=="-"){let A=O.slice(1);if(A.length>1&&!A.includes("=")){for(let V of A)n[V]=!0;t+=1;continue}let T=A,U=e[t+1];if(U!==void 0&&!U.startsWith("-"))n[T]=U,t+=2;else n[T]=!0,t+=1;continue}u.push(O),t+=1}let d=u[0],i,r,m=1;if(d==="memory"&&(u[1]==="collections"||u[1]==="collection"))r="collections",i=u[2],m=i?3:2;else i=u[1],m=i?2:1;return{group:d,action:i,nest:r,flags:n,positionals:u.slice(m)}}function M(e,...n){for(let u of n){let t=e[u];if(typeof t==="string"&&t.length>0)return t}return}function K(e,...n){for(let u of n){let t=e[u];if(t===!0)return!0;if(typeof t==="string"){let y=t.toLowerCase();if(y==="1"||y==="true"||y==="yes")return!0}}return!1}function F(e){return K(e,"help","h")}function q(e){return K(e,"version","v","V")}function s(e,n){if(n.quiet&&!n.json)return;if(n.json||!process.stdout.isTTY){process.stdout.write(`${JSON.stringify({ok:!0,data:e},null,2)}
|
|
4
|
+
`);return}if(e===void 0||e===null){process.stdout.write(`ok
|
|
5
|
+
`);return}if(typeof e==="string"){process.stdout.write(`${e}
|
|
6
|
+
`);return}process.stdout.write(`${Je(e)}
|
|
7
|
+
`)}function ie(e,n){let{message:u,code:t,details:y}=Le(e);if(n.json||!process.stdout.isTTY)process.stdout.write(`${JSON.stringify({ok:!1,error:{code:Ke(t),message:u,details:y??null}},null,2)}
|
|
8
|
+
`);else if(process.stderr.write(`error: ${u}
|
|
9
|
+
`),y!==void 0&&process.env.DUN_DEBUG==="1")process.stderr.write(`${JSON.stringify(y,null,2)}
|
|
10
|
+
`);return t}function Ke(e){switch(e){case o.AUTH:return"auth";case o.FORBIDDEN:return"forbidden";case o.VALIDATION:return"validation";case o.NOT_FOUND:return"not_found";case o.RATE_LIMITED:return"rate_limited";case o.NETWORK:return"network";default:return"usage"}}function Le(e){if(e instanceof w)return{message:e.message,code:e.code,details:e.details};if(e instanceof Error)return{message:e.message,code:o.USAGE};return{message:String(e),code:o.USAGE}}function Je(e){if(Array.isArray(e)){if(e.length===0)return"(empty)";if(e.every((n)=>n&&typeof n==="object"))return te(e);return e.map((n)=>`- ${P(n)}`).join(`
|
|
11
|
+
`)}if(e&&typeof e==="object"){let n=e;if(Array.isArray(n.items)){let u=typeof n.total==="number"?`total=${n.total} showing=${n.items.length}`:void 0,t=te(n.items);return u?`${u}
|
|
12
|
+
${t}`:t}return Object.entries(n).map(([u,t])=>`${u}: ${P(t)}`).join(`
|
|
13
|
+
`)}return String(e)}function te(e){if(e.length===0)return"(empty)";let n=["id","name","slug","status","providerName","username","displayName","role","scheduledAt","content","text","originalFilename","score","collectionId"],u=new Set;for(let m of e)for(let O of Object.keys(m))u.add(O);let t=[...n.filter((m)=>u.has(m)),...[...u].filter((m)=>!n.includes(m)).slice(0,6)].slice(0,8),y=e.map((m)=>t.map((O)=>He(P(m[O]),O==="content"||O==="text"?48:28))),d=t.map((m,O)=>Math.max(m.length,...y.map((A)=>A[O]?.length??0))),i=t.map((m,O)=>m.padEnd(d[O])).join(" "),r=y.map((m)=>m.map((O,A)=>O.padEnd(d[A])).join(" ")).join(`
|
|
14
|
+
`);return`${i}
|
|
15
|
+
${r}`}function P(e){if(e===null||e===void 0)return"";if(typeof e==="string")return e.replace(/\s+/g," ").trim();if(typeof e==="number"||typeof e==="boolean")return String(e);if(e instanceof Date)return e.toISOString();if(typeof e==="object"){let n=e;if(typeof n.id==="string"&&typeof n.name==="string")return`${n.name} (${n.id})`;if(typeof n.status==="string")return n.status;return JSON.stringify(e)}return String(e)}function He(e,n){if(e.length<=n)return e;return`${e.slice(0,Math.max(0,n-1))}…`}var G={OK:0,USAGE:1,AUTH:2,FORBIDDEN:3,VALIDATION:4,NOT_FOUND:5,RATE_LIMITED:6,NETWORK:7};class S extends Error{code;details;constructor(e,n=G.USAGE,u){super(e);this.name="CliError",this.code=n,this.details=u}}import{mkdir as Ye,readFile as Rn,writeFile as ze,chmod as We,unlink as Me}from"node:fs/promises";import{dirname as Qe,join as me}from"node:path";import{homedir as Xe}from"node:os";var Q="0.2.0";function E(){if(process.env.DUN_CONFIG)return process.env.DUN_CONFIG;let e=process.env.XDG_CONFIG_HOME,n=e?e:me(Xe(),".config");return me(n,"dunsocial","config.json")}function L(){return E()}async function J(e){let n=E();await Ye(Qe(n),{recursive:!0});let u={...e,updatedAt:new Date().toISOString()};await ze(n,`${JSON.stringify(u,null,2)}
|
|
16
|
+
`,"utf8");try{await We(n,384)}catch{}}async function oe(){let e=E();try{await Me(e)}catch(n){if(n.code!=="ENOENT")throw new w(`Failed to clear config: ${n.message}`,o.USAGE)}}function R(e){if(!e.token)throw new w("Not authenticated. Run `dun auth login --token <token>` or set DUN_TOKEN.",o.AUTH);return e.token}function k(e){if(!e.workspaceId)throw new w("No workspace selected. Run `dun workspace use <id|slug>` or pass --workspace / DUN_WORKSPACE_ID.",o.VALIDATION);return e.workspaceId}var X=`dun — DunSocial agent-first CLI v${Q}
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
dun <command> [flags]
|
|
20
|
+
|
|
21
|
+
Auth:
|
|
22
|
+
dun auth login # device-code browser flow
|
|
23
|
+
dun auth login --token <token> # CI / headless
|
|
24
|
+
dun auth login --no-browser
|
|
25
|
+
dun auth logout
|
|
26
|
+
dun auth whoami
|
|
27
|
+
dun auth status
|
|
28
|
+
|
|
29
|
+
Workspaces:
|
|
30
|
+
dun workspace list
|
|
31
|
+
dun workspace use <id|slug>
|
|
32
|
+
dun workspace current
|
|
33
|
+
|
|
34
|
+
Accounts:
|
|
35
|
+
dun accounts list [--platform x]
|
|
36
|
+
|
|
37
|
+
Posts:
|
|
38
|
+
dun posts list [--status scheduled|published|failed|draft|cancelled]
|
|
39
|
+
dun posts get <id>
|
|
40
|
+
dun posts schedule --text "..." --accounts <id,id> (--at ISO | --in 2h) [--media id,id]
|
|
41
|
+
dun posts publish --text "..." --accounts <id,id> [--media id,id] [--natural]
|
|
42
|
+
dun posts reschedule <id> --at ISO|--in 2h [--text "..."]
|
|
43
|
+
dun posts cancel <id>
|
|
44
|
+
dun posts delete <id> [--yes]
|
|
45
|
+
dun posts x-cap
|
|
46
|
+
|
|
47
|
+
Drafts:
|
|
48
|
+
dun drafts list
|
|
49
|
+
dun drafts get <id>
|
|
50
|
+
dun drafts create --text "..." [--platforms x,linkedin] [--accounts id,id]
|
|
51
|
+
dun drafts update <id> --text "..."
|
|
52
|
+
dun drafts delete <id> [--yes]
|
|
53
|
+
|
|
54
|
+
Media:
|
|
55
|
+
dun media list
|
|
56
|
+
dun media get <id>
|
|
57
|
+
dun media upload <file> [--alt "..."]
|
|
58
|
+
dun media delete <id> [--yes]
|
|
59
|
+
|
|
60
|
+
Memory:
|
|
61
|
+
dun memory collections list
|
|
62
|
+
dun memory collections create --name "..." [--color blue] [--private]
|
|
63
|
+
dun memory list --collection <id>
|
|
64
|
+
dun memory save --collection <id> --text "..."
|
|
65
|
+
dun memory search --prompt "..." --collections <id,id> [--top-k 5]
|
|
66
|
+
dun memory delete <id> --collection <id> [--yes]
|
|
67
|
+
|
|
68
|
+
Agent skills:
|
|
69
|
+
dun skill install claude
|
|
70
|
+
dun skill install codex
|
|
71
|
+
dun skill path
|
|
72
|
+
|
|
73
|
+
Global flags:
|
|
74
|
+
--json Machine-readable output (default when non-TTY / CI)
|
|
75
|
+
--workspace <id> Override workspace for this command
|
|
76
|
+
--api-url <url> Override API base (default https://api.dunsocial.com)
|
|
77
|
+
--token <token> Override bearer token for this command
|
|
78
|
+
--debug Log HTTP method/path/status to stderr
|
|
79
|
+
--quiet Suppress human success output
|
|
80
|
+
--yes Skip destructive confirmations
|
|
81
|
+
--help, -h Show help
|
|
82
|
+
--version, -v Show version
|
|
83
|
+
|
|
84
|
+
Env:
|
|
85
|
+
DUN_TOKEN DUN_WORKSPACE_ID DUN_API_URL DUN_CONFIG DUN_JSON=1 DUN_DEBUG=1
|
|
86
|
+
|
|
87
|
+
Exit codes:
|
|
88
|
+
0 ok · 1 usage · 2 auth · 3 forbidden · 4 validation · 5 not found · 6 rate limit · 7 network
|
|
89
|
+
|
|
90
|
+
Docs: https://dunsocial.com · MCP remains the remote-host path; CLI is local/CI.
|
|
91
|
+
`;function f(e){return{auth:`dun auth
|
|
92
|
+
|
|
93
|
+
login [--token] Device-code browser login (default) or --token for CI
|
|
94
|
+
logout Clear local config
|
|
95
|
+
whoami Show current user profile
|
|
96
|
+
status Show auth + workspace summary
|
|
97
|
+
`,workspace:`dun workspace
|
|
98
|
+
|
|
99
|
+
list List workspaces you belong to
|
|
100
|
+
use <id|slug> Set default workspace
|
|
101
|
+
current Show default workspace
|
|
102
|
+
`,workspaces:"Alias of workspace",accounts:"dun accounts list [--platform <name>]",posts:`dun posts
|
|
103
|
+
|
|
104
|
+
list [--status] [--account] [--limit] [--offset]
|
|
105
|
+
get <id>
|
|
106
|
+
schedule --text --accounts (--at|--in) [--media]
|
|
107
|
+
publish --text --accounts [--media] [--natural]
|
|
108
|
+
reschedule <id> (--at|--in) [--text]
|
|
109
|
+
cancel <id>
|
|
110
|
+
delete <id> [--yes]
|
|
111
|
+
x-cap
|
|
112
|
+
`,drafts:"dun drafts list|get|create|update|delete",media:"dun media list|get|upload|delete",memory:`dun memory collections list|create
|
|
113
|
+
dun memory list|save|search|delete`,skill:`dun skill install claude|codex
|
|
114
|
+
dun skill path`}[e]??X}function N(e,n){if(n.quiet&&!n.json)return;if(n.json||!process.stdout.isTTY){process.stdout.write(`${JSON.stringify({ok:!0,data:e},null,2)}
|
|
115
|
+
`);return}if(e===void 0||e===null){process.stdout.write(`ok
|
|
116
|
+
`);return}if(typeof e==="string"){process.stdout.write(`${e}
|
|
117
|
+
`);return}process.stdout.write(`${Ze(e)}
|
|
118
|
+
`)}function Ze(e){if(Array.isArray(e)){if(e.length===0)return"(empty)";if(e.every((n)=>n&&typeof n==="object"))return ye(e);return e.map((n)=>`- ${v(n)}`).join(`
|
|
119
|
+
`)}if(e&&typeof e==="object"){let n=e;if(Array.isArray(n.items)){let u=typeof n.total==="number"?`total=${n.total} showing=${n.items.length}`:void 0,t=ye(n.items);return u?`${u}
|
|
120
|
+
${t}`:t}return Object.entries(n).map(([u,t])=>`${u}: ${v(t)}`).join(`
|
|
121
|
+
`)}return String(e)}function ye(e){if(e.length===0)return"(empty)";let n=["id","name","slug","status","providerName","username","displayName","role","scheduledAt","content","text","originalFilename","score","collectionId"],u=new Set;for(let m of e)for(let O of Object.keys(m))u.add(O);let t=[...n.filter((m)=>u.has(m)),...[...u].filter((m)=>!n.includes(m)).slice(0,6)].slice(0,8),y=e.map((m)=>t.map((O)=>Be(v(m[O]),O==="content"||O==="text"?48:28))),d=t.map((m,O)=>Math.max(m.length,...y.map((A)=>A[O]?.length??0))),i=t.map((m,O)=>m.padEnd(d[O])).join(" "),r=y.map((m)=>m.map((O,A)=>O.padEnd(d[A])).join(" ")).join(`
|
|
122
|
+
`);return`${i}
|
|
123
|
+
${r}`}function v(e){if(e===null||e===void 0)return"";if(typeof e==="string")return e.replace(/\s+/g," ").trim();if(typeof e==="number"||typeof e==="boolean")return String(e);if(e instanceof Date)return e.toISOString();if(typeof e==="object"){let n=e;if(typeof n.id==="string"&&typeof n.name==="string")return`${n.name} (${n.id})`;if(typeof n.status==="string")return n.status;return JSON.stringify(e)}return String(e)}function Be(e,n){if(e.length<=n)return e;return`${e.slice(0,Math.max(0,n-1))}…`}function H(e,n,u){if(!e)return;if(u!==void 0){process.stderr.write(`[debug] ${n} ${JSON.stringify(u)}
|
|
124
|
+
`);return}process.stderr.write(`[debug] ${n}
|
|
125
|
+
`)}class we{ctx;constructor(e){this.ctx=e}async request(e){let n=e.method??"GET",u=new URL(e.path.startsWith("http")?e.path:`${this.ctx.apiUrl}${e.path}`);if(e.query)for(let[m,O]of Object.entries(e.query)){if(O===void 0||O===null||O==="")continue;u.searchParams.set(m,String(O))}let t={Accept:"application/json","User-Agent":`dunsocial-cli/${Q}`,...e.headers??{}};if(e.auth!==!1)t.Authorization=`Bearer ${R(this.ctx)}`;if(e.workspace)t["X-Workspace-Id"]=k(this.ctx);let y;if(e.body!==void 0)t["Content-Type"]="application/json",y=JSON.stringify(e.body);H(this.ctx.debug,`${n} ${u.toString()}`);let d;try{d=await fetch(u,{method:n,headers:t,body:y})}catch(m){throw new w(`Network error: ${m.message}`,o.NETWORK,{cause:String(m)})}let i=await d.text(),r=null;if(i)try{r=JSON.parse(i)}catch{r=null}if(H(this.ctx.debug,`← ${d.status}`,{ok:d.ok,bodyPreview:i.slice(0,300)}),!d.ok){let m=r?.error||r?.message||(i?i.slice(0,300):`HTTP ${d.status}`);throw new w(m,ne(d.status),{status:d.status,body:r??i})}if(r&&typeof r==="object"&&"success"in r){if(r.success===!1)throw new w(r.error||r.message||"Request failed",o.VALIDATION,r);return r.data}return r??void 0}get(e,n){return this.request({...n,method:"GET",path:e})}post(e,n,u){return this.request({...u,method:"POST",path:e,body:n})}patch(e,n,u){return this.request({...u,method:"PATCH",path:e,body:n})}delete(e,n,u){return this.request({...u,method:"DELETE",path:e,body:n})}}function D(e){return new we(e)}function $(e,...n){for(let u of n){let t=e[u];if(typeof t==="string"&&t.length>0)return t}return}function j(e,...n){for(let u of n){let t=e[u];if(t===!0)return!0;if(typeof t==="string"){let y=t.toLowerCase();if(y==="1"||y==="true"||y==="yes")return!0}}return!1}function I(e,...n){let u=$(e,...n);if(u===void 0)return;let t=Number(u);if(!Number.isFinite(t))throw Error(`Invalid number for --${n[0]}: ${u}`);return t}function p(e,...n){let u=$(e,...n);if(u===void 0)return;return u.split(",").map((t)=>t.trim()).filter(Boolean)}async function Oe(e,n,u,t){switch(n){case"login":return he(e,u);case"logout":return Pe(e);case"whoami":return se(e);case"status":return Se(e);default:throw new w(`Unknown auth command "${n??""}". Try: login, logout, whoami, status`,o.USAGE)}}async function he(e,n){let u=$(n,"token")||process.env.DUN_TOKEN;if(u)return Fe(e,u);return qe(e,n)}async function Fe(e,n){let u={...e,token:n},y=await D(u).get("/api/user/profile");await J({...e.config,apiUrl:e.apiUrl,token:n,defaultWorkspaceId:e.config.defaultWorkspaceId,defaultWorkspaceSlug:e.config.defaultWorkspaceSlug,defaultWorkspaceName:e.config.defaultWorkspaceName}),N({user:{id:y.id,name:y.name,email:y.email},configPath:L(),method:"token",message:"Logged in. Next: dun workspace list && dun workspace use <id|slug>"},e)}async function qe(e,n){let u=D({...e,token:void 0}),t=await u.post("/api/cli/auth/start",{clientName:"dun-cli"},{auth:!1}),y=!j(n,"no-browser");if(!e.json)process.stderr.write(`
|
|
126
|
+
DunSocial CLI login
|
|
127
|
+
`),process.stderr.write(`───────────────────
|
|
128
|
+
`),process.stderr.write(`In your browser, open:
|
|
129
|
+
${t.verificationUrlComplete}
|
|
130
|
+
|
|
131
|
+
`),process.stderr.write(`Or go to ${t.verificationUrl} and enter code:
|
|
132
|
+
`),process.stderr.write(` ${t.userCode}
|
|
133
|
+
|
|
134
|
+
`),process.stderr.write(`Waiting for approval…
|
|
135
|
+
`);else process.stderr.write(JSON.stringify({ok:!0,phase:"waiting",userCode:t.userCode,verificationUrlComplete:t.verificationUrlComplete})+`
|
|
136
|
+
`);if(y&&process.stdout.isTTY)try{await fe(t.verificationUrlComplete)}catch{}let d=Math.max(3,t.interval||5)*1000,i=Date.now()+(t.expiresIn||900)*1000;while(Date.now()<i){await Ee(d);let r=await u.post("/api/cli/auth/poll",{deviceCode:t.deviceCode},{auth:!1});if(r.status==="pending")continue;if(r.status==="expired")throw new w("Login expired. Run dun auth login again.",o.AUTH);if(r.status==="denied")throw new w("Login was denied in the browser.",o.AUTH);if(r.status==="approved"){let m=r.token,O={...e,token:m},A=D(O),T=await A.get("/api/user/profile"),U={...e.config,apiUrl:e.apiUrl,token:m,defaultWorkspaceId:r.workspaceId||e.config.defaultWorkspaceId,defaultWorkspaceSlug:e.config.defaultWorkspaceSlug,defaultWorkspaceName:e.config.defaultWorkspaceName};if(r.workspaceId)try{let b=(await A.get("/api/workspaces")).find((_)=>_.id===r.workspaceId);if(b)U.defaultWorkspaceId=b.id,U.defaultWorkspaceSlug=b.slug,U.defaultWorkspaceName=b.name}catch{}await J(U),N({user:{id:T.id,name:T.name,email:T.email},workspaceId:U.defaultWorkspaceId??null,configPath:L(),method:"device",message:U.defaultWorkspaceId?"Logged in.":"Logged in. Next: dun workspace list && dun workspace use <id|slug>"},e);return}}throw new w("Login timed out. Run dun auth login again.",o.AUTH)}async function Pe(e){await oe(),N({message:"Logged out. Config cleared.",configPath:L()},e)}async function se(e){R(e);let u=await D(e).get("/api/user/profile");N({id:u.id,name:u.name,email:u.email,image:u.image??null,apiUrl:e.apiUrl,workspaceId:e.workspaceId??null},e)}async function Se(e){let n=Boolean(e.token),u=null,t=null;if(n)try{u=await D(e).get("/api/user/profile")}catch(y){t=y instanceof Error?y.message:String(y)}N({authenticated:Boolean(u),tokenPresent:n,apiUrl:e.apiUrl,workspaceId:e.workspaceId??null,workspaceSlug:e.config.defaultWorkspaceSlug??null,workspaceName:e.config.defaultWorkspaceName??null,configPath:L(),user:u?{id:u.id,name:u.name,email:u.email}:null,error:t},e)}function Ee(e){return new Promise((n)=>setTimeout(n,e))}async function fe(e){let n=process.platform,u=n==="darwin"?["open",e]:n==="win32"?["cmd","/c","start","",e]:["xdg-open",e];await Bun.spawn(u,{stdout:"ignore",stderr:"ignore"}).exited}async function Ne(e,n,u,t){R(e);let y=D(e);switch(n){case"list":case"ls":{let d=await y.get("/api/workspaces");N(d.map((i)=>({id:i.id,name:i.name,slug:i.slug,role:i.role,timezone:i.timezone,plan:i.subscription?.planName??null,status:i.subscription?.status??null})),e);return}case"use":{let d=t[0];if(!d)throw new w("Usage: dun workspace use <id|slug>",o.USAGE);let r=(await y.get("/api/workspaces")).find((m)=>m.id===d||m.slug===d);if(!r)throw new w(`Workspace not found: ${d}. Run dun workspace list`,o.NOT_FOUND);await J({...e.config,apiUrl:e.apiUrl,token:e.token,defaultWorkspaceId:r.id,defaultWorkspaceSlug:r.slug,defaultWorkspaceName:r.name}),N({id:r.id,name:r.name,slug:r.slug,role:r.role,message:`Default workspace set to ${r.name}`},e);return}case"current":{if(!e.workspaceId&&!e.config.defaultWorkspaceId)throw new w("No workspace selected. Run dun workspace use <id|slug>",o.VALIDATION);let d=e.workspaceId||e.config.defaultWorkspaceId;try{let r=(await y.get("/api/workspaces")).find((m)=>m.id===d||m.slug===d);if(r){N({id:r.id,name:r.name,slug:r.slug,role:r.role,timezone:r.timezone},e);return}}catch{}N({id:d,name:e.config.defaultWorkspaceName??null,slug:e.config.defaultWorkspaceSlug??null},e);return}default:throw new w(`Unknown workspace command "${n??""}". Try: list, use, current`,o.USAGE)}}async function Ae(e,n,u,t){R(e),k(e);let y=D(e);switch(n){case void 0:case"list":case"ls":{let d=await y.get("/api/social-accounts",{workspace:!0}),i=$(u,"platform","p")?.toLowerCase(),r=i?d.filter((m)=>{let O=(m.providerName||"").toLowerCase();return({x:["x","twitter"],twitter:["x","twitter"],linkedin:["linkedin"],"linkedin-business":["linkedin business","linkedin-business","linkedin_business"],bluesky:["bluesky"],threads:["threads"],reddit:["reddit"],pinterest:["pinterest"],instagram:["instagram"],youtube:["youtube"]}[i]??[i]).some((U)=>O.includes(U))}):d;N(r.map((m)=>({id:m.id,providerName:m.providerName,username:m.username??null,displayName:m.displayName??null,isConnected:m.isConnected,expiresAt:m.expiresAt??null})),e);return}default:throw new w(`Unknown accounts command "${n}". Try: list`,o.USAGE)}}var ve=/^(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|week|weeks)$/i;function C(e,n=new Date){let u=e.trim();if(!u)throw new w("Empty time value",o.VALIDATION);let t=u.match(ve);if(t){let i=Number(t[1]),r=t[2].toLowerCase(),m=Ce(r)*i;return new Date(n.getTime()+m)}let y=u.match(/^in\s+(.+)$/i);if(y)return C(y[1],n);let d=new Date(u);if(Number.isNaN(d.getTime()))throw new w(`Invalid time "${e}". Use ISO-8601 (2026-06-01T09:00:00Z) or relative (2h, 30m, 1d).`,o.VALIDATION);return d}function Ce(e){switch(e){case"s":case"sec":case"secs":case"second":case"seconds":return 1000;case"m":case"min":case"mins":case"minute":case"minutes":return 60000;case"h":case"hr":case"hrs":case"hour":case"hours":return 3600000;case"d":case"day":case"days":return 86400000;case"w":case"week":case"weeks":return 604800000;default:throw new w(`Unknown time unit: ${e}`,o.VALIDATION)}}function Z(e){if(e.at&&e.in)throw new w("Pass only one of --at or --in",o.VALIDATION);if(e.at)return C(e.at).toISOString();if(e.in)return C(e.in).toISOString();throw new w("Schedule time required. Pass --at <iso> or --in <duration> (e.g. 2h).",o.VALIDATION)}function Ue(e){if(!e)return[];return e.split(",").map((n)=>n.trim()).filter(Boolean)}function $e(e){let n=$(e,"text","content","c","t");if(!n)throw new w("Missing --text",o.VALIDATION);return n}function c(e){let n=p(e,"accounts","account","a")??Ue($(e,"accounts","account","a"));if(!n||n.length===0)throw new w("Missing --accounts <id,id>",o.VALIDATION);return n}function l(e){return p(e,"media","media-urls")??Ue($(e,"media","media-urls"))}function Y(e){let n=$(e,"meta","metadata","meta-json");if(!n)return;try{let u=JSON.parse(n);if(!u||typeof u!=="object"||Array.isArray(u))throw Error("metadata must be a JSON object");return u}catch(u){throw new w(`Invalid --meta JSON: ${u.message}`,o.VALIDATION)}}async function Te(e,n,u,t){R(e),k(e);let y=D(e);switch(n){case"list":case"ls":{let d=await y.get("/api/posts",{workspace:!0,query:{status:$(u,"status","s"),socialAccountId:$(u,"account","social-account"),groupId:$(u,"group"),limit:I(u,"limit")??50,offset:I(u,"offset")??0}});N(d,e);return}case"get":{let d=t[0];if(!d)throw new w("Usage: dun posts get <id>",o.USAGE);let i=await y.get(`/api/posts/${d}`,{workspace:!0});N(i,e);return}case"schedule":{let d=$e(u),i=c(u),r=Z({at:$(u,"at"),in:$(u,"in")}),m=await y.post("/api/posts/schedule",{content:d,socialAccountIds:i,scheduledAt:r,mediaUrls:l(u),metadata:Y(u)??{}},{workspace:!0});N(m,e);return}case"publish":case"publish-now":{let d=$e(u),i=c(u),r=await y.post("/api/posts/publish-now",{content:d,socialAccountIds:i,mediaUrls:l(u),metadata:Y(u)??{},naturalPosting:j(u,"natural","natural-posting")},{workspace:!0});N(r,e);return}case"reschedule":{let d=t[0];if(!d)throw new w("Usage: dun posts reschedule <id> --at|--in",o.USAGE);let r={scheduledAt:Z({at:$(u,"at"),in:$(u,"in")})},m=$(u,"text","content","c","t");if(m)r.content=m;let O=l(u);if(O.length)r.mediaUrls=O;let A=Y(u);if(A)r.metadata=A;if(!r.content){let U=await y.get(`/api/posts/${d}`,{workspace:!0});if(r.content=U.content,!r.metadata&&U.metadata)r.metadata=U.metadata;if(!r.mediaUrls&&U.mediaUrls)r.mediaUrls=U.mediaUrls}let T=await y.post(`/api/posts/${d}/reschedule`,r,{workspace:!0});N(T,e);return}case"cancel":{let d=t[0];if(!d)throw new w("Usage: dun posts cancel <id>",o.USAGE);let i=await y.post(`/api/posts/${d}/cancel`,void 0,{workspace:!0});N(i??{id:d,status:"cancelled"},e);return}case"delete":case"rm":{let d=t[0];if(!d)throw new w("Usage: dun posts delete <id> [--yes]",o.USAGE);if(!e.yes&&!j(u,"yes","y"))throw new w("Refusing to delete without --yes (or CI/yes mode)",o.VALIDATION);let i=await y.delete(`/api/posts/${d}`,void 0,{workspace:!0});N(i??{id:d,deleted:!0},e);return}case"x-cap":case"xcap":{let d=await y.get("/api/posts/x-cap-usage",{workspace:!0});N(d,e);return}case"schedule-thread":case"publish-thread":case"publish-thread-now":{let d=$(u,"file","f"),i=$(u,"account","accounts","a")??c(u)[0];if(!i)throw new w("Missing --account <xAccountId>",o.VALIDATION);let r;if(d){let O=await Bun.file(d).text(),A=JSON.parse(O);if(Array.isArray(A))r=A;else if(A&&typeof A==="object"&&Array.isArray(A.tweets))r=A.tweets;else throw new w("Thread file must be an array of tweets or { tweets: [] }",o.VALIDATION)}else{let O=$(u,"text","content");if(!O)throw new w("Provide --file thread.json (array of {content}) or informal --text with || separators is not enough alone for threads — use --file",o.VALIDATION);r=O.split("||").map((A)=>({content:A.trim()})).filter((A)=>A.content)}if(r.length<2)throw new w("Threads require at least 2 tweets",o.VALIDATION);if(n==="schedule-thread"){let O=Z({at:$(u,"at"),in:$(u,"in")}),A=await y.post("/api/posts/schedule-thread",{socialAccountId:i,tweets:r,scheduledAt:O,metadata:Y(u)??{}},{workspace:!0});N(A,e);return}let m=await y.post("/api/posts/publish-thread-now",{socialAccountId:i,tweets:r,metadata:Y(u)??{}},{workspace:!0});N(m,e);return}default:throw new w(`Unknown posts command "${n??""}". Try: list, get, schedule, publish, reschedule, cancel, delete, x-cap, schedule-thread, publish-thread`,o.USAGE)}}async function De(e,n,u,t){R(e),k(e);let y=D(e);switch(n){case"list":case"ls":{let d=await y.get("/api/drafts",{workspace:!0,query:{limit:I(u,"limit")??50,offset:I(u,"offset")??0}});N(d,e);return}case"get":{let d=t[0];if(!d)throw new w("Usage: dun drafts get <id>",o.USAGE);let i=await y.get(`/api/drafts/${d}`,{workspace:!0});N(i,e);return}case"create":{let d=$(u,"text","content","c","t");if(!d)throw new w("Missing --text",o.VALIDATION);let i=await y.post("/api/drafts",{content:d,name:$(u,"name","n"),socialAccountIds:p(u,"accounts","account","a")??[],selectedPlatforms:p(u,"platforms","platform","p")??[],mediaUrls:p(u,"media")??[]},{workspace:!0});N(i,e);return}case"update":{let d=t[0];if(!d)throw new w("Usage: dun drafts update <id> --text ...",o.USAGE);let i={id:d},r=$(u,"text","content","c","t");if(r)i.content=r;let m=$(u,"name","n");if(m!==void 0)i.name=m;let O=p(u,"accounts","account","a");if(O)i.socialAccountIds=O;let A=p(u,"platforms","platform","p");if(A)i.selectedPlatforms=A;let T=p(u,"media");if(T)i.mediaUrls=T;let U=await y.patch(`/api/drafts/${d}`,i,{workspace:!0});N(U,e);return}case"delete":case"rm":{let d=t[0];if(!d)throw new w("Usage: dun drafts delete <id> [--yes]",o.USAGE);if(!e.yes&&!j(u,"yes","y"))throw new w("Refusing to delete without --yes",o.VALIDATION);let i=await y.delete(`/api/drafts/${d}`,void 0,{workspace:!0});N(i??{id:d,deleted:!0},e);return}default:throw new w(`Unknown drafts command "${n??""}". Try: list, get, create, update, delete`,o.USAGE)}}import{basename as ce}from"node:path";function le(e){let n=e.split(".").pop()?.toLowerCase()??"";return{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",mp4:"video/mp4",mov:"video/quicktime",webm:"video/webm"}[n]??"application/octet-stream"}async function Re(e,n,u,t){R(e),k(e);let y=D(e);switch(n){case"list":case"ls":{let d=await y.get("/api/media",{workspace:!0,query:{limit:I(u,"limit")??50,offset:I(u,"offset")??0}});N(d,e);return}case"get":{let d=t[0];if(!d)throw new w("Usage: dun media get <id>",o.USAGE);let i=await y.get(`/api/media/${d}`,{workspace:!0});N(i,e);return}case"upload":{let d=t[0]||$(u,"file","f");if(!d)throw new w('Usage: dun media upload <file> [--alt "..."]',o.USAGE);let i=Bun.file(d);if(!await i.exists())throw new w(`File not found: ${d}`,o.VALIDATION);let m=ce(d),O=$(u,"mime","type","content-type")||i.type||le(m),A=i.size,T=$(u,"alt","alt-text"),U=await y.post("/api/media/upload-url",{filename:m,mimeType:O,fileSize:A},{workspace:!0});H(e.debug,`PUT ${U.uploadUrl}`,{storageKey:U.storageKey,fileSize:A});let V=await i.arrayBuffer(),b;try{b=await fetch(U.uploadUrl,{method:"PUT",headers:{"Content-Type":O,"Content-Length":String(A)},body:V})}catch(z){throw new w(`Upload PUT failed: ${z.message}`,o.NETWORK)}if(!b.ok){let z=await b.text().catch(()=>"");throw new w(`Upload PUT failed with HTTP ${b.status}${z?`: ${z.slice(0,200)}`:""}`,o.NETWORK)}let _=await y.post("/api/media/complete",{storageKey:U.storageKey,originalFilename:m,mimeType:O,fileSize:A,altText:T},{workspace:!0});N(_,e);return}case"delete":case"rm":{let d=t[0];if(!d)throw new w("Usage: dun media delete <id> [--yes]",o.USAGE);if(!e.yes&&!j(u,"yes","y"))throw new w("Refusing to delete without --yes",o.VALIDATION);let i=await y.delete(`/api/media/${d}`,void 0,{workspace:!0});N(i??{id:d,deleted:!0},e);return}default:throw new w(`Unknown media command "${n??""}". Try: list, get, upload, delete`,o.USAGE)}}async function be(e,n,u,t){R(e),k(e);let y=D(e),d=n[0];if(d==="collections"||d==="collection"){let i=n[1]||"list";switch(i){case"list":case"ls":{let r=await y.get("/api/memory/collections",{workspace:!0});N(r,e);return}case"create":{let r=$(u,"name","n");if(!r)throw new w("Missing --name",o.VALIDATION);let m=await y.post("/api/memory/collections",{name:r,color:$(u,"color")??"blue",isPrivate:j(u,"private")},{workspace:!0});N(m,e);return}case"get":{let r=n[2]||t[0];if(!r)throw new w("Usage: dun memory collections get <id>",o.USAGE);let m=await y.get(`/api/memory/collections/${r}`,{workspace:!0});N(m,e);return}default:throw new w(`Unknown memory collections command "${i}". Try: list, create, get`,o.USAGE)}}switch(d){case"list":case"ls":{let i=$(u,"collection","collection-id","c");if(!i)throw new w("Missing --collection <id>",o.VALIDATION);let r=await y.get("/api/memory",{workspace:!0,query:{collectionId:i,limit:I(u,"limit")??100}});N(r,e);return}case"save":case"add":{let i=$(u,"collection","collection-id","c"),r=$(u,"text","t");if(!i)throw new w("Missing --collection <id>",o.VALIDATION);if(!r)throw new w("Missing --text",o.VALIDATION);let m=await y.post("/api/memory",{collectionId:i,text:r},{workspace:!0});N(m,e);return}case"search":{let i=$(u,"prompt","q","query","text"),r=p(u,"collections","collection","collection-ids","c")??[];if(!i)throw new w("Missing --prompt",o.VALIDATION);if(r.length===0)throw new w("Missing --collections <id,id>",o.VALIDATION);let m=await y.post("/api/memory/search",{prompt:i,collectionIds:r,topK:I(u,"top-k","topk","k")??5},{workspace:!0});N(m,e);return}case"delete":case"rm":{let i=n[1]||t[0],r=$(u,"collection","collection-id","c");if(!i)throw new w("Usage: dun memory delete <id> --collection <id>",o.USAGE);if(!r)throw new w("Missing --collection <id>",o.VALIDATION);if(!e.yes&&!j(u,"yes","y"))throw new w("Refusing to delete without --yes",o.VALIDATION);let m=await y.delete(`/api/memory/${i}`,{collectionId:r,memoryId:i},{workspace:!0});N(m??{id:i,deleted:!0},e);return}default:throw new w(`Unknown memory command "${d??""}". Try: collections, list, save, search, delete`,o.USAGE)}}import{mkdir as g,writeFile as x,readFile as ke}from"node:fs/promises";import{dirname as B,join as h}from"node:path";import{homedir as a}from"node:os";import{fileURLToPath as ge}from"node:url";function ee(){let e=B(ge(import.meta.url));return h(e,"..","..","skills","dunsocial","SKILL.md")}async function xe(){try{return await ke(ee(),"utf8")}catch{throw new w(`Bundled skill not found at ${ee()}. Reinstall @dunsocial/cli.`,o.USAGE)}}async function je(e,n,u,t){switch(n){case"path":{N({path:ee()},e);return}case"install":{let y=(t[0]||"").toLowerCase();if(!y||y!=="claude"&&y!=="codex")throw new w("Usage: dun skill install claude|codex",o.USAGE);let d=await xe();if(y==="claude"){let _=h(a(),".claude","skills","dunsocial","SKILL.md");await g(B(_),{recursive:!0}),await x(_,d,"utf8"),N({target:"claude",path:_,message:"Installed DunSocial skill for Claude Code. Restart or open a new session if needed."},e);return}let i=h(a(),".codex","skills","dunsocial","SKILL.md");await g(B(i),{recursive:!0}),await x(i,d,"utf8");let r=h(a(),".codex","AGENTS.md");await g(B(r),{recursive:!0});let m="";try{m=await ke(r,"utf8")}catch{m=""}let O=["<!-- BEGIN DUNSOCIAL CLI SKILL -->","## DunSocial CLI","","Use the `dun` CLI for DunSocial scheduling from the shell. Prefer `--json` output.",`Skill details: ${i}`,"","Common flow:","1. `dun auth status --json`","2. `dun workspace list --json` / `dun workspace use <id>`","3. `dun accounts list --json`",'4. `dun posts schedule --text "..." --accounts <id> --in 1h --json`',"<!-- END DUNSOCIAL CLI SKILL -->"].join(`
|
|
137
|
+
`),A="<!-- BEGIN DUNSOCIAL CLI SKILL -->",T="<!-- END DUNSOCIAL CLI SKILL -->",U,V=m.indexOf(A),b=m.indexOf(T);if(V!==-1&&b!==-1&&b>V)U=m.slice(0,V)+O+m.slice(b+T.length);else if(m.trim())U=`${m.trimEnd()}
|
|
138
|
+
|
|
139
|
+
${O}
|
|
140
|
+
`;else U=`${O}
|
|
141
|
+
`;await x(r,U,"utf8"),N({target:"codex",skillPath:i,agentsPath:r,message:"Installed DunSocial skill for Codex."},e);return}default:throw new w(`Unknown skill command "${n??""}". Try: install, path`,o.USAGE)}}async function ae(e){let n=re(e),{group:u,action:t,nest:y,flags:d,positionals:i}=n;if(q(d)&&!u)return process.stdout.write(`${W}
|
|
142
|
+
`),G.OK;if(u==="help"||!u&&F(d)){let m=u==="help"?t||i[0]:void 0;return process.stdout.write(m?`${f(m)}
|
|
143
|
+
`:`${X}
|
|
144
|
+
`),G.OK}if(!u)return process.stdout.write(`${X}
|
|
145
|
+
`),G.OK;if(F(d))return process.stdout.write(`${f(u)}
|
|
146
|
+
`),G.OK;let r=await de({apiUrl:M(d,"api-url","apiUrl"),token:M(d,"token"),workspace:M(d,"workspace","w"),json:K(d,"json","j")?!0:void 0,debug:K(d,"debug"),quiet:K(d,"quiet","q"),yes:K(d,"yes","y")});if(q(d))return s({version:W},r),G.OK;try{switch(u){case"auth":await Oe(r,t,d,i);break;case"workspace":case"workspaces":await Ne(r,t,d,i);break;case"account":case"accounts":await Ae(r,t??"list",d,i);break;case"post":case"posts":await Te(r,t,d,i);break;case"draft":case"drafts":await De(r,t,d,i);break;case"media":await Re(r,t,d,i);break;case"memory":{let m=y?[y,t].filter(Boolean):t?[t]:[];await be(r,m,d,i);break}case"skill":case"skills":await je(r,t,d,i);break;case"version":s({version:W},r);break;default:throw new S(`Unknown command "${u}". Run dun --help`,G.USAGE)}return G.OK}catch(m){return ie(m,r)}}var en=await ae(process.argv.slice(2));process.exit(en);
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dunsocial",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "DunSocial agent-first CLI — schedule and publish social posts from the terminal, CI, and coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/thisuxhq/dunpostsapp.git",
|
|
10
|
+
"directory": "apps/cli"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/thisuxhq/dunpostsapp/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://dunsocial.com",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dunsocial",
|
|
18
|
+
"cli",
|
|
19
|
+
"social-media",
|
|
20
|
+
"scheduler",
|
|
21
|
+
"ai-agent",
|
|
22
|
+
"mcp"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"dun": "./dist/index.js",
|
|
26
|
+
"dunsocial": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"skills",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"packageManager": "bun@1.3.8",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"dev": "bun run src/index.ts",
|
|
43
|
+
"start": "bun run src/index.ts",
|
|
44
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --minify --sourcemap=none && bun run scripts/postbuild.ts",
|
|
45
|
+
"type-check": "tsc --noEmit",
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"release-check": "bun run scripts/release-check.ts",
|
|
48
|
+
"prepack": "bun run build && bun run release-check"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"@types/node": "^22",
|
|
53
|
+
"typescript": "^5.9.2"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dunsocial
|
|
3
|
+
description: Schedule, publish, and manage DunSocial posts, drafts, media, and memory from the shell via the dun CLI. Use when the user wants to post or schedule to X/LinkedIn/Bluesky/Threads/Reddit/Pinterest/Instagram/YouTube through DunSocial, manage drafts or media uploads, search workspace memory, or wire social publishing into CI/agent workflows.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DunSocial CLI skill
|
|
7
|
+
|
|
8
|
+
Use the `dun` binary (`@dunsocial/cli`). Prefer **`--json`** on every command so output is machine-parseable.
|
|
9
|
+
|
|
10
|
+
Remote hosts (ChatGPT web, Claude web) should keep using the DunSocial **MCP** server. This CLI is for **local agents, terminals, and CI**.
|
|
11
|
+
|
|
12
|
+
## Auth + workspace
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Interactive (opens browser device login)
|
|
16
|
+
dun auth login
|
|
17
|
+
|
|
18
|
+
# CI / headless — prefer PAT from Settings → CLI (dun_pat_…)
|
|
19
|
+
export DUN_TOKEN="$DUN_PAT" DUN_WORKSPACE_ID="$DUN_WORKSPACE_ID"
|
|
20
|
+
# legacy full session also works:
|
|
21
|
+
dun auth login --token "$DUN_TOKEN" --json
|
|
22
|
+
|
|
23
|
+
dun auth status --json
|
|
24
|
+
dun workspace list --json
|
|
25
|
+
dun workspace use <id-or-slug> --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Env overrides (highest priority for CI):
|
|
29
|
+
|
|
30
|
+
- `DUN_TOKEN`
|
|
31
|
+
- `DUN_WORKSPACE_ID`
|
|
32
|
+
- `DUN_API_URL` (default `https://api.dunsocial.com`)
|
|
33
|
+
|
|
34
|
+
Config file: `~/.config/dunsocial/config.json` (mode 0600).
|
|
35
|
+
|
|
36
|
+
## Discovery order (always)
|
|
37
|
+
|
|
38
|
+
1. `dun auth status --json` — confirm auth
|
|
39
|
+
2. `dun workspace current --json` or `list` + `use`
|
|
40
|
+
3. `dun accounts list --json` — capture social account **ids**
|
|
41
|
+
4. Act (schedule/publish/draft/media/memory)
|
|
42
|
+
|
|
43
|
+
Do **not** invent account IDs. Do **not** treat `drafts create` as a successful publish.
|
|
44
|
+
|
|
45
|
+
## Schedule a post
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dun posts schedule \
|
|
49
|
+
--text "Shipped the DunSocial CLI for agents." \
|
|
50
|
+
--accounts acc_x,acc_li \
|
|
51
|
+
--in 1h \
|
|
52
|
+
--json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or absolute time:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
dun posts schedule --text "..." --accounts acc_x --at 2026-06-01T09:00:00Z --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Publish now
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
dun posts publish --text "Hello from dun" --accounts acc_x --json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Upload media then post
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
dun media upload ./shot.png --alt "Product screenshot" --json
|
|
71
|
+
# use returned asset id:
|
|
72
|
+
dun posts schedule --text "..." --accounts acc_x --media <assetId> --in 30m --json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Drafts
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dun drafts create --text "WIP idea" --platforms x,linkedin --json
|
|
79
|
+
dun drafts list --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Memory
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
dun memory collections list --json
|
|
86
|
+
dun memory save --collection <id> --text "We never promise same-day enterprise onboarding." --json
|
|
87
|
+
dun memory search --prompt "onboarding promises" --collections <id> --json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Threads (X)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# thread.json => [ {"content":"1"}, {"content":"2"} ] or { "tweets": [...] }
|
|
94
|
+
dun posts schedule-thread --account acc_x --file ./thread.json --in 2h --json
|
|
95
|
+
dun posts publish-thread --account acc_x --file ./thread.json --json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Destructive ops
|
|
99
|
+
|
|
100
|
+
Require `--yes` (or `CI=1`) for `delete` commands.
|
|
101
|
+
|
|
102
|
+
## Exit codes
|
|
103
|
+
|
|
104
|
+
| Code | Meaning |
|
|
105
|
+
|------|---------|
|
|
106
|
+
| 0 | ok |
|
|
107
|
+
| 1 | usage |
|
|
108
|
+
| 2 | auth |
|
|
109
|
+
| 3 | forbidden |
|
|
110
|
+
| 4 | validation |
|
|
111
|
+
| 5 | not found |
|
|
112
|
+
| 6 | rate limited |
|
|
113
|
+
| 7 | network |
|
|
114
|
+
|
|
115
|
+
On failure with `--json`:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{ "ok": false, "error": { "code": "validation", "message": "...", "details": null } }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Pitfalls
|
|
122
|
+
|
|
123
|
+
- Workspace is required for almost every command after login (`workspace use` or `--workspace` / `DUN_WORKSPACE_ID`).
|
|
124
|
+
- `--accounts` takes DunSocial **social account ids**, not handles.
|
|
125
|
+
- Local files must be uploaded via `media upload` before referencing in posts (unless you already have an asset id / storage key).
|
|
126
|
+
- Platform-specific metadata (Reddit/IG/Pinterest) can be passed as `--meta '{"reddit":{...}}'` JSON when needed; prefer dedicated product flows in the dashboard for complex cases.
|
|
127
|
+
- MCP OAuth scopes and CLI bearer sessions are different paths — fixing one does not fix the other.
|
|
128
|
+
|
|
129
|
+
## Help
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
dun --help
|
|
133
|
+
dun posts --help
|
|
134
|
+
```
|