noslop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruben
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,170 @@
1
+ # noslop
2
+
3
+ > Let Claude Code manage your X posts - no slop, just good content
4
+
5
+ [![npm version](https://img.shields.io/npm/v/noslop.svg)](https://www.npmjs.com/package/noslop)
6
+ [![CI](https://github.com/rubenartus/noslop/actions/workflows/ci.yml/badge.svg)](https://github.com/rubenartus/noslop/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node.js Version](https://img.shields.io/node/v/noslop.svg)](https://nodejs.org)
9
+
10
+ A CLI tool designed for AI assistants like Claude Code to draft, schedule, and manage your social media posts. All content lives as simple markdown files in folders - easy to version control, easy for AI to work with, easy for you to review.
11
+
12
+ The TUI gives you a quick way to check what's scheduled, mark posts as ready, and keep your AI on track.
13
+
14
+ ## Screenshot
15
+
16
+ ![noslop TUI](./screenshot.jpg)
17
+
18
+ ## Features
19
+
20
+ - **AI-first CLI** - Built for Claude Code to draft, schedule, and reschedule posts on demand
21
+ - **Simple markdown files** - All content stored as `.md` files in `drafts/` and `posts/` folders
22
+ - **Human-friendly TUI** - Review and direct the work with keyboard shortcuts
23
+ - **Content workflow** - Draft → Ready → Post → Published pipeline
24
+ - **Schedule view** - Calendar view of what's coming up
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ # npm
30
+ npm install -g noslop
31
+
32
+ # pnpm
33
+ pnpm add -g noslop
34
+
35
+ # Or run directly with npx
36
+ npx noslop
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```bash
42
+ # Initialize a new project
43
+ noslop init
44
+
45
+ # Create a new draft
46
+ noslop new "My First Post"
47
+
48
+ # Open interactive TUI
49
+ noslop
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ ### Core Commands
55
+
56
+ | Command | Description |
57
+ |---------|-------------|
58
+ | `noslop` | Open interactive TUI |
59
+ | `noslop init [name]` | Initialize new project |
60
+ | `noslop help` | Show all commands |
61
+
62
+ ### Content Management
63
+
64
+ | Command | Description |
65
+ |---------|-------------|
66
+ | `noslop new <title>` | Create new draft |
67
+ | `noslop list [-d\|--drafts] [-p\|--posts]` | List content |
68
+ | `noslop status` | Show project summary |
69
+ | `noslop show <id>` | Show full content |
70
+
71
+ ### Workflow Actions
72
+
73
+ | Command | Description |
74
+ |---------|-------------|
75
+ | `noslop ready <id>` | Mark draft as ready |
76
+ | `noslop unready <id>` | Mark as in-progress |
77
+ | `noslop post <id>` | Move to posts folder |
78
+ | `noslop unpost <id>` | Move back to drafts |
79
+ | `noslop publish <id> <url>` | Add published URL |
80
+ | `noslop schedule <id> <datetime>` | Set schedule (YYYY-MM-DD HH:MM) |
81
+ | `noslop delete <id>` | Delete a draft |
82
+
83
+ ## TUI Keyboard Shortcuts
84
+
85
+ | Key | Action |
86
+ |-----|--------|
87
+ | `Tab` | Switch between Drafts/Posts |
88
+ | `↑/↓` | Navigate items |
89
+ | `Enter` | Toggle ready (drafts) / Add URL (posts) |
90
+ | `Space` | Move to Posts / Move to Drafts |
91
+ | `Backspace` | Delete draft (in-progress only) |
92
+ | `s` | Toggle schedule view |
93
+ | `←/→` | Navigate weeks (schedule view) |
94
+ | `q` | Quit |
95
+
96
+ ## Project Structure
97
+
98
+ After running `noslop init`, your project will have:
99
+
100
+ ```
101
+ your-project/
102
+ ├── CLAUDE.md # Your brand voice & guidelines
103
+ ├── NOSLOP.md # CLI documentation (auto-generated)
104
+ ├── drafts/ # Work in progress
105
+ │ └── post-name/
106
+ │ ├── x.md # Post content
107
+ │ └── assets/
108
+ └── posts/ # Published content
109
+ └── post-name/
110
+ └── x.md
111
+ ```
112
+
113
+ ## Post File Format
114
+
115
+ Each post is stored in `x.md` with this structure:
116
+
117
+ ```markdown
118
+ # Post Title
119
+
120
+ ## Post
121
+ \`\`\`
122
+ Your post content here
123
+ \`\`\`
124
+
125
+ ## Status
126
+ draft | ready
127
+
128
+ ## Media
129
+ Description of media to attach
130
+
131
+ ## Scheduled
132
+ 2026-01-27 09:00
133
+
134
+ ## Posted
135
+ 2026-01-27 09:15
136
+
137
+ ## Published
138
+ https://x.com/user/status/123
139
+ ```
140
+
141
+ ## Working with AI Assistants
142
+
143
+ noslop is designed to work with Claude Code and other AI assistants:
144
+
145
+ 1. **CLAUDE.md** - Add your brand voice, tone guidelines, and content rules
146
+ 2. **NOSLOP.md** - Auto-generated CLI documentation for AI reference
147
+ 3. Use CLI commands instead of manual file editing
148
+
149
+ ## Requirements
150
+
151
+ - Node.js 18 or higher
152
+ - Terminal with Unicode support
153
+
154
+ ## Contributing
155
+
156
+ Contributions are welcome! Please feel free to submit a Pull Request.
157
+
158
+ 1. Fork the repository
159
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
160
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
161
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
162
+ 5. Open a Pull Request
163
+
164
+ ## License
165
+
166
+ [MIT](LICENSE)
167
+
168
+ ---
169
+
170
+ Made with love for content creators who prefer the terminal
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { isNoslopProject, getAllContent, findItem, createDraft, updateStatus, updateSchedule, moveToPosts, moveToDrafts, addPublishedUrl, deleteDraft, sortBySchedule, } from './lib/content.js';
4
+ import { initProject, ensureNoslopMd } from './lib/templates.js';
5
+ const program = new Command();
6
+ program
7
+ .name('noslop')
8
+ .description('Social media content workflow CLI - no slop, just good content')
9
+ .version('0.1.0');
10
+ // Default command: open TUI
11
+ program.action(async () => {
12
+ if (!isNoslopProject()) {
13
+ console.log('Not a noslop project. Run `noslop init` first.');
14
+ process.exit(1);
15
+ }
16
+ // Ensure NOSLOP.md is up to date with current version
17
+ ensureNoslopMd();
18
+ // Dynamic import to avoid loading React/Ink for non-TUI commands
19
+ const { renderTUI } = await import('./tui.js');
20
+ renderTUI();
21
+ });
22
+ // Init command
23
+ program
24
+ .command('init')
25
+ .description('Initialize a new noslop project in current directory')
26
+ .argument('[name]', 'Project name (optional)')
27
+ .action((name) => {
28
+ if (isNoslopProject()) {
29
+ console.log('Already a noslop project (drafts/ or posts/ exists).');
30
+ process.exit(1);
31
+ }
32
+ initProject(process.cwd(), name);
33
+ console.log('Initialized noslop project:');
34
+ console.log(' - drafts/ (your work in progress)');
35
+ console.log(' - posts/ (published content)');
36
+ console.log(' - CLAUDE.md (your brand guidelines)');
37
+ console.log(' - NOSLOP.md (tool documentation)');
38
+ console.log('');
39
+ console.log('Next: Run `noslop new "Your First Post"` to create a draft.');
40
+ });
41
+ // New draft command
42
+ program
43
+ .command('new')
44
+ .description('Create a new draft')
45
+ .argument('<title>', 'Title for the new draft')
46
+ .action((title) => {
47
+ if (!isNoslopProject()) {
48
+ console.log('Not a noslop project. Run `noslop init` first.');
49
+ process.exit(1);
50
+ }
51
+ const item = createDraft(title);
52
+ console.log(`Created draft: ${item.folder}/`);
53
+ console.log(` Edit: ${item.xFile}`);
54
+ });
55
+ // List command
56
+ program
57
+ .command('list')
58
+ .description('List all content')
59
+ .option('-d, --drafts', 'Show only drafts')
60
+ .option('-p, --posts', 'Show only posts')
61
+ .action((options) => {
62
+ if (!isNoslopProject()) {
63
+ console.log('Not a noslop project. Run `noslop init` first.');
64
+ process.exit(1);
65
+ }
66
+ const { drafts, posts } = getAllContent();
67
+ if (!options.posts) {
68
+ const sortedDrafts = sortBySchedule(drafts);
69
+ if (sortedDrafts.length > 0) {
70
+ console.log('DRAFTS:');
71
+ for (const d of sortedDrafts) {
72
+ const status = d.status === 'ready' ? '✓' : '○';
73
+ const schedule = d.scheduledAt ? ` (${d.scheduledAt})` : '';
74
+ console.log(` ${status} ${d.folder}${schedule}`);
75
+ }
76
+ }
77
+ else {
78
+ console.log('DRAFTS: (none)');
79
+ }
80
+ }
81
+ if (!options.drafts) {
82
+ if (!options.posts) {
83
+ console.log('');
84
+ }
85
+ if (posts.length > 0) {
86
+ console.log('POSTS:');
87
+ for (const p of posts) {
88
+ const url = p.published?.startsWith('http') ? ' ✓' : '';
89
+ const date = p.postedAt ? ` (${p.postedAt})` : '';
90
+ console.log(` ${p.folder}${date}${url}`);
91
+ }
92
+ }
93
+ else {
94
+ console.log('POSTS: (none)');
95
+ }
96
+ }
97
+ });
98
+ // Status command
99
+ program
100
+ .command('status')
101
+ .description('Show project status summary')
102
+ .action(() => {
103
+ if (!isNoslopProject()) {
104
+ console.log('Not a noslop project. Run `noslop init` first.');
105
+ process.exit(1);
106
+ }
107
+ const { drafts, posts } = getAllContent();
108
+ const ready = drafts.filter(d => d.status === 'ready').length;
109
+ const wip = drafts.length - ready;
110
+ const published = posts.filter(p => p.published?.startsWith('http')).length;
111
+ console.log(`Drafts: ${drafts.length} (${ready} ready, ${wip} in progress)`);
112
+ console.log(`Posts: ${posts.length} (${published} with URL)`);
113
+ // Next scheduled
114
+ const scheduled = sortBySchedule(drafts).filter(d => d.scheduledAt);
115
+ if (scheduled.length > 0) {
116
+ console.log(`Next: ${scheduled[0].folder} @ ${scheduled[0].scheduledAt}`);
117
+ }
118
+ });
119
+ // Show command
120
+ program
121
+ .command('show')
122
+ .description('Show full content of a post/draft')
123
+ .argument('<id>', 'Folder name or ID')
124
+ .action((id) => {
125
+ if (!isNoslopProject()) {
126
+ console.log('Not a noslop project. Run `noslop init` first.');
127
+ process.exit(1);
128
+ }
129
+ const item = findItem(id);
130
+ if (!item) {
131
+ console.log(`Not found: ${id}`);
132
+ process.exit(1);
133
+ }
134
+ console.log(`# ${item.title}`);
135
+ console.log(`Folder: ${item.folder}`);
136
+ console.log(`Status: ${item.status}`);
137
+ if (item.scheduledAt) {
138
+ console.log(`Scheduled: ${item.scheduledAt}`);
139
+ }
140
+ if (item.postedAt) {
141
+ console.log(`Posted: ${item.postedAt}`);
142
+ }
143
+ if (item.published) {
144
+ console.log(`URL: ${item.published}`);
145
+ }
146
+ if (item.media !== 'None') {
147
+ console.log(`Media: ${item.media}`);
148
+ }
149
+ console.log('---');
150
+ console.log(item.post);
151
+ });
152
+ // Ready command
153
+ program
154
+ .command('ready')
155
+ .description('Mark draft as ready to post')
156
+ .argument('<id>', 'Folder name or ID')
157
+ .action((id) => {
158
+ if (!isNoslopProject()) {
159
+ console.log('Not a noslop project. Run `noslop init` first.');
160
+ process.exit(1);
161
+ }
162
+ const item = findItem(id);
163
+ if (!item) {
164
+ console.log(`Not found: ${id}`);
165
+ process.exit(1);
166
+ }
167
+ updateStatus(item, 'ready');
168
+ console.log(`Marked as ready: ${item.folder}`);
169
+ });
170
+ // Unready command
171
+ program
172
+ .command('unready')
173
+ .description('Mark draft as in-progress')
174
+ .argument('<id>', 'Folder name or ID')
175
+ .action((id) => {
176
+ if (!isNoslopProject()) {
177
+ console.log('Not a noslop project. Run `noslop init` first.');
178
+ process.exit(1);
179
+ }
180
+ const item = findItem(id);
181
+ if (!item) {
182
+ console.log(`Not found: ${id}`);
183
+ process.exit(1);
184
+ }
185
+ updateStatus(item, 'draft');
186
+ console.log(`Marked as in-progress: ${item.folder}`);
187
+ });
188
+ // Post command (move to posts)
189
+ program
190
+ .command('post')
191
+ .description('Move draft to posts folder')
192
+ .argument('<id>', 'Folder name or ID')
193
+ .action((id) => {
194
+ if (!isNoslopProject()) {
195
+ console.log('Not a noslop project. Run `noslop init` first.');
196
+ process.exit(1);
197
+ }
198
+ const item = findItem(id);
199
+ if (!item) {
200
+ console.log(`Not found: ${id}`);
201
+ process.exit(1);
202
+ }
203
+ moveToPosts(item);
204
+ console.log(`Moved to posts: ${item.folder}`);
205
+ });
206
+ // Unpost command (move back to drafts)
207
+ program
208
+ .command('unpost')
209
+ .description('Move post back to drafts')
210
+ .argument('<id>', 'Folder name or ID')
211
+ .action((id) => {
212
+ if (!isNoslopProject()) {
213
+ console.log('Not a noslop project. Run `noslop init` first.');
214
+ process.exit(1);
215
+ }
216
+ const item = findItem(id);
217
+ if (!item) {
218
+ console.log(`Not found: ${id}`);
219
+ process.exit(1);
220
+ }
221
+ moveToDrafts(item);
222
+ console.log(`Moved to drafts: ${item.folder}`);
223
+ });
224
+ // Publish command
225
+ program
226
+ .command('publish')
227
+ .description('Add published URL to post')
228
+ .argument('<id>', 'Folder name or ID')
229
+ .argument('<url>', 'Published URL')
230
+ .action((id, url) => {
231
+ if (!isNoslopProject()) {
232
+ console.log('Not a noslop project. Run `noslop init` first.');
233
+ process.exit(1);
234
+ }
235
+ const item = findItem(id);
236
+ if (!item) {
237
+ console.log(`Not found: ${id}`);
238
+ process.exit(1);
239
+ }
240
+ addPublishedUrl(item, url);
241
+ console.log(`Published: ${item.folder}`);
242
+ console.log(` URL: ${url}`);
243
+ });
244
+ // Schedule command
245
+ program
246
+ .command('schedule')
247
+ .description('Set/update scheduled time')
248
+ .argument('<id>', 'Folder name or ID')
249
+ .argument('<datetime>', 'Date and time (YYYY-MM-DD HH:MM)')
250
+ .action((id, datetime) => {
251
+ if (!isNoslopProject()) {
252
+ console.log('Not a noslop project. Run `noslop init` first.');
253
+ process.exit(1);
254
+ }
255
+ const item = findItem(id);
256
+ if (!item) {
257
+ console.log(`Not found: ${id}`);
258
+ process.exit(1);
259
+ }
260
+ updateSchedule(item, datetime);
261
+ console.log(`Scheduled: ${item.folder} @ ${datetime}`);
262
+ });
263
+ // Delete command
264
+ program
265
+ .command('delete')
266
+ .description('Delete a draft')
267
+ .argument('<id>', 'Folder name or ID')
268
+ .action((id) => {
269
+ if (!isNoslopProject()) {
270
+ console.log('Not a noslop project. Run `noslop init` first.');
271
+ process.exit(1);
272
+ }
273
+ const item = findItem(id);
274
+ if (!item) {
275
+ console.log(`Not found: ${id}`);
276
+ process.exit(1);
277
+ }
278
+ deleteDraft(item);
279
+ console.log(`Deleted: ${item.folder}`);
280
+ });
281
+ program.parse();
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Status of a content item (draft or ready to post)
3
+ */
4
+ export type ContentStatus = 'draft' | 'ready';
5
+ /**
6
+ * Represents a content item (draft or post)
7
+ */
8
+ export interface ContentItem {
9
+ id: string;
10
+ folder: string;
11
+ title: string;
12
+ post: string;
13
+ published: string;
14
+ media: string;
15
+ postedAt: string;
16
+ scheduledAt: string;
17
+ status: ContentStatus;
18
+ path: string;
19
+ xFile: string;
20
+ }
21
+ /**
22
+ * Validate that a string is a valid URL
23
+ * @param str - String to validate
24
+ * @returns True if valid URL, false otherwise
25
+ */
26
+ export declare function isValidUrl(str: string): boolean;
27
+ /**
28
+ * Get the paths to posts and drafts directories
29
+ * @param cwd - Working directory (defaults to process.cwd())
30
+ * @returns Object with postsDir and draftsDir paths
31
+ */
32
+ export declare function getContentDirs(cwd?: string): {
33
+ postsDir: string;
34
+ draftsDir: string;
35
+ };
36
+ /**
37
+ * Check if the current directory is a noslop project
38
+ * @param cwd - Working directory to check
39
+ * @returns True if drafts/ or posts/ directory exists
40
+ */
41
+ export declare function isNoslopProject(cwd?: string): boolean;
42
+ /**
43
+ * Parse content from a directory into ContentItem objects
44
+ * @param dir - Directory containing content folders
45
+ * @param prefix - ID prefix ('D' for drafts, 'P' for posts)
46
+ * @returns Array of parsed content items, sorted alphabetically by folder name
47
+ */
48
+ export declare function parseContent(dir: string, prefix: string): ContentItem[];
49
+ /**
50
+ * Get all drafts from the drafts directory
51
+ * @param cwd - Working directory
52
+ * @returns Array of draft content items
53
+ */
54
+ export declare function getDrafts(cwd?: string): ContentItem[];
55
+ /**
56
+ * Get all posts from the posts directory
57
+ * @param cwd - Working directory
58
+ * @returns Array of post content items
59
+ */
60
+ export declare function getPosts(cwd?: string): ContentItem[];
61
+ /**
62
+ * Get all content (drafts + posts)
63
+ * @param cwd - Working directory
64
+ * @returns Object with drafts and posts arrays
65
+ */
66
+ export declare function getAllContent(cwd?: string): {
67
+ drafts: ContentItem[];
68
+ posts: ContentItem[];
69
+ };
70
+ /**
71
+ * Find a content item by folder name or ID
72
+ * @param query - Folder name (exact or partial) or ID (D001, P001)
73
+ * @param cwd - Working directory
74
+ * @returns Matching content item or null if not found
75
+ * @throws Error if query contains path traversal patterns
76
+ * @example
77
+ * findItem('monday-motivation') // exact folder match
78
+ * findItem('monday') // partial folder match
79
+ * findItem('D001') // ID match
80
+ */
81
+ export declare function findItem(query: string, cwd?: string): ContentItem | null;
82
+ /**
83
+ * Create a new draft with the given title
84
+ * @param title - Title for the draft (will be slugified for folder name)
85
+ * @param cwd - Working directory
86
+ * @returns The created ContentItem
87
+ * @throws Error if draft creation fails
88
+ */
89
+ export declare function createDraft(title: string, cwd?: string): ContentItem;
90
+ /**
91
+ * Update the status of a content item
92
+ * @param item - Content item to update
93
+ * @param newStatus - New status ('draft' or 'ready')
94
+ * @throws Error if file operations fail
95
+ */
96
+ export declare function updateStatus(item: ContentItem, newStatus: ContentStatus): void;
97
+ /**
98
+ * Update the scheduled date of a content item
99
+ * @param item - Content item to update
100
+ * @param datetime - Scheduled datetime string (YYYY-MM-DD HH:MM)
101
+ * @throws Error if file operations fail
102
+ */
103
+ export declare function updateSchedule(item: ContentItem, datetime: string): void;
104
+ /**
105
+ * Move a draft to the posts folder
106
+ * @param item - Content item to move
107
+ * @param cwd - Working directory
108
+ * @throws Error if move operation fails
109
+ */
110
+ export declare function moveToPosts(item: ContentItem, cwd?: string): void;
111
+ /**
112
+ * Move a post back to the drafts folder
113
+ * @param item - Content item to move
114
+ * @param cwd - Working directory
115
+ * @throws Error if move operation fails
116
+ */
117
+ export declare function moveToDrafts(item: ContentItem, cwd?: string): void;
118
+ /**
119
+ * Add a published URL to a post
120
+ * @param item - Content item to update
121
+ * @param url - Published URL
122
+ * @throws Error if URL is invalid or file operations fail
123
+ */
124
+ export declare function addPublishedUrl(item: ContentItem, url: string): void;
125
+ /**
126
+ * Delete a draft
127
+ * @param item - Content item to delete
128
+ * @throws Error if delete operation fails
129
+ */
130
+ export declare function deleteDraft(item: ContentItem): void;
131
+ /**
132
+ * Format a date as YYYY-MM-DD HH:MM
133
+ * @param date - Date to format
134
+ * @returns Formatted date string
135
+ */
136
+ export declare function formatDate(date: Date): string;
137
+ /**
138
+ * Sort content items by scheduled date (oldest first)
139
+ * Unscheduled items are placed at the end
140
+ * @param items - Array of content items to sort
141
+ * @returns New sorted array (does not mutate original)
142
+ */
143
+ export declare function sortBySchedule(items: ContentItem[]): ContentItem[];