noslop 0.1.0 → 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.
@@ -0,0 +1,188 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ /**
5
+ * Get the noslop config directory path
6
+ * Uses ~/.config/noslop following XDG Base Directory spec
7
+ */
8
+ export function getConfigDir() {
9
+ const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
10
+ return path.join(xdgConfig, 'noslop');
11
+ }
12
+ /**
13
+ * Get the X credentials directory path
14
+ */
15
+ export function getXCredentialsDir() {
16
+ return path.join(getConfigDir(), 'credentials', 'x');
17
+ }
18
+ /**
19
+ * Ensure config directories exist with proper permissions
20
+ */
21
+ export function ensureConfigDirs() {
22
+ const configDir = getConfigDir();
23
+ const xCredentialsDir = getXCredentialsDir();
24
+ if (!fs.existsSync(configDir)) {
25
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
26
+ }
27
+ if (!fs.existsSync(xCredentialsDir)) {
28
+ fs.mkdirSync(xCredentialsDir, { recursive: true, mode: 0o700 });
29
+ }
30
+ }
31
+ /**
32
+ * Validate account name (alphanumeric, dashes, underscores)
33
+ */
34
+ export function isValidAccountName(name) {
35
+ return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0 && name.length <= 50;
36
+ }
37
+ /**
38
+ * Get the path to X credentials file for an account
39
+ */
40
+ export function getXCredentialsPath(accountName) {
41
+ if (!isValidAccountName(accountName)) {
42
+ throw new Error(`Invalid account name: ${accountName}`);
43
+ }
44
+ return path.join(getXCredentialsDir(), `${accountName}.json`);
45
+ }
46
+ /**
47
+ * Check if X credentials exist for an account
48
+ */
49
+ export function hasXCredentials(accountName) {
50
+ try {
51
+ return fs.existsSync(getXCredentialsPath(accountName));
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Load X credentials from disk for an account
59
+ * @returns Credentials object or null if not found
60
+ */
61
+ export function loadXCredentials(accountName) {
62
+ let credPath;
63
+ try {
64
+ credPath = getXCredentialsPath(accountName);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ if (!fs.existsSync(credPath)) {
70
+ return null;
71
+ }
72
+ try {
73
+ const content = fs.readFileSync(credPath, 'utf-8');
74
+ return JSON.parse(content);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * Save X credentials to disk with restricted permissions
82
+ * @param accountName - Account name to save under
83
+ * @param credentials - Credentials to save
84
+ */
85
+ export function saveXCredentials(accountName, credentials) {
86
+ ensureConfigDirs();
87
+ const credPath = getXCredentialsPath(accountName);
88
+ const content = JSON.stringify(credentials, null, 2);
89
+ // Write with restricted permissions (owner read/write only)
90
+ fs.writeFileSync(credPath, content, { mode: 0o600 });
91
+ }
92
+ /**
93
+ * Delete X credentials from disk
94
+ */
95
+ export function deleteXCredentials(accountName) {
96
+ let credPath;
97
+ try {
98
+ credPath = getXCredentialsPath(accountName);
99
+ }
100
+ catch {
101
+ return;
102
+ }
103
+ if (fs.existsSync(credPath)) {
104
+ fs.unlinkSync(credPath);
105
+ }
106
+ }
107
+ /**
108
+ * List all X accounts with stored credentials
109
+ */
110
+ export function listXAccounts() {
111
+ const dir = getXCredentialsDir();
112
+ if (!fs.existsSync(dir)) {
113
+ return [];
114
+ }
115
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
116
+ const accounts = [];
117
+ for (const file of files) {
118
+ const name = file.replace('.json', '');
119
+ const creds = loadXCredentials(name);
120
+ accounts.push({
121
+ name,
122
+ screenName: creds?.screenName,
123
+ });
124
+ }
125
+ return accounts;
126
+ }
127
+ /**
128
+ * Validate that credentials have all required fields
129
+ */
130
+ export function validateXCredentials(creds) {
131
+ return !!(creds.apiKey && creds.apiSecret && creds.accessToken && creds.accessTokenSecret);
132
+ }
133
+ /**
134
+ * Get the X account name from project's CLAUDE.md
135
+ */
136
+ export function getProjectXAccount(cwd = process.cwd()) {
137
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
138
+ if (!fs.existsSync(claudeMdPath)) {
139
+ return null;
140
+ }
141
+ try {
142
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
143
+ const match = content.match(/## X Account\n([^\n#]+)/);
144
+ if (match) {
145
+ const accountName = match[1].trim();
146
+ return isValidAccountName(accountName) ? accountName : null;
147
+ }
148
+ return null;
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ /**
155
+ * Set the X account name in project's CLAUDE.md
156
+ */
157
+ export function setProjectXAccount(accountName, cwd = process.cwd()) {
158
+ if (!isValidAccountName(accountName)) {
159
+ throw new Error(`Invalid account name: ${accountName}`);
160
+ }
161
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
162
+ if (!fs.existsSync(claudeMdPath)) {
163
+ throw new Error('CLAUDE.md not found. Run `noslop init` first.');
164
+ }
165
+ let content = fs.readFileSync(claudeMdPath, 'utf-8');
166
+ if (content.includes('## X Account\n')) {
167
+ // Update existing section
168
+ content = content.replace(/## X Account\n[^\n#]*/, `## X Account\n${accountName}`);
169
+ }
170
+ else {
171
+ // Add new section at the end
172
+ content = `${content.trim()}\n\n## X Account\n${accountName}\n`;
173
+ }
174
+ fs.writeFileSync(claudeMdPath, content);
175
+ }
176
+ /**
177
+ * Remove the X account from project's CLAUDE.md
178
+ */
179
+ export function removeProjectXAccount(cwd = process.cwd()) {
180
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
181
+ if (!fs.existsSync(claudeMdPath)) {
182
+ return;
183
+ }
184
+ let content = fs.readFileSync(claudeMdPath, 'utf-8');
185
+ // Remove the X Account section
186
+ content = content.replace(/\n*## X Account\n[^\n#]*\n?/, '\n');
187
+ fs.writeFileSync(claudeMdPath, `${content.trim()}\n`);
188
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,226 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { getConfigDir, getXCredentialsDir, ensureConfigDirs, getXCredentialsPath, hasXCredentials, loadXCredentials, saveXCredentials, deleteXCredentials, validateXCredentials, isValidAccountName, listXAccounts, getProjectXAccount, setProjectXAccount, removeProjectXAccount, } from './config.js';
6
+ describe('config', () => {
7
+ const originalXdgConfig = process.env.XDG_CONFIG_HOME;
8
+ let testConfigDir;
9
+ let testProjectDir;
10
+ beforeEach(() => {
11
+ // Use a temp directory for tests
12
+ testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'noslop-test-'));
13
+ testProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'noslop-project-'));
14
+ process.env.XDG_CONFIG_HOME = testConfigDir;
15
+ });
16
+ afterEach(() => {
17
+ // Restore original env
18
+ if (originalXdgConfig) {
19
+ process.env.XDG_CONFIG_HOME = originalXdgConfig;
20
+ }
21
+ else {
22
+ delete process.env.XDG_CONFIG_HOME;
23
+ }
24
+ // Clean up test directories
25
+ if (testConfigDir && fs.existsSync(testConfigDir)) {
26
+ fs.rmSync(testConfigDir, { recursive: true });
27
+ }
28
+ if (testProjectDir && fs.existsSync(testProjectDir)) {
29
+ fs.rmSync(testProjectDir, { recursive: true });
30
+ }
31
+ });
32
+ describe('getConfigDir', () => {
33
+ it('returns XDG_CONFIG_HOME/noslop when XDG_CONFIG_HOME is set', () => {
34
+ const dir = getConfigDir();
35
+ expect(dir).toBe(path.join(testConfigDir, 'noslop'));
36
+ });
37
+ it('returns ~/.config/noslop when XDG_CONFIG_HOME is not set', () => {
38
+ delete process.env.XDG_CONFIG_HOME;
39
+ const dir = getConfigDir();
40
+ expect(dir).toBe(path.join(os.homedir(), '.config', 'noslop'));
41
+ });
42
+ });
43
+ describe('getXCredentialsDir', () => {
44
+ it('returns config dir + credentials/x', () => {
45
+ const dir = getXCredentialsDir();
46
+ expect(dir).toBe(path.join(testConfigDir, 'noslop', 'credentials', 'x'));
47
+ });
48
+ });
49
+ describe('ensureConfigDirs', () => {
50
+ it('creates config and credentials directories', () => {
51
+ ensureConfigDirs();
52
+ expect(fs.existsSync(getConfigDir())).toBe(true);
53
+ expect(fs.existsSync(getXCredentialsDir())).toBe(true);
54
+ });
55
+ it('does not fail if directories already exist', () => {
56
+ ensureConfigDirs();
57
+ ensureConfigDirs(); // Should not throw
58
+ expect(fs.existsSync(getConfigDir())).toBe(true);
59
+ });
60
+ });
61
+ describe('isValidAccountName', () => {
62
+ it('accepts valid names', () => {
63
+ expect(isValidAccountName('personal')).toBe(true);
64
+ expect(isValidAccountName('work')).toBe(true);
65
+ expect(isValidAccountName('client-acme')).toBe(true);
66
+ expect(isValidAccountName('test_123')).toBe(true);
67
+ });
68
+ it('rejects invalid names', () => {
69
+ expect(isValidAccountName('')).toBe(false);
70
+ expect(isValidAccountName('has spaces')).toBe(false);
71
+ expect(isValidAccountName('has/slash')).toBe(false);
72
+ expect(isValidAccountName('../traversal')).toBe(false);
73
+ expect(isValidAccountName('a'.repeat(51))).toBe(false);
74
+ });
75
+ });
76
+ describe('X credentials', () => {
77
+ const accountName = 'personal';
78
+ const validCreds = {
79
+ apiKey: 'test-api-key',
80
+ apiSecret: 'test-api-secret',
81
+ accessToken: 'test-access-token',
82
+ accessTokenSecret: 'test-access-token-secret',
83
+ screenName: 'testuser',
84
+ accountId: 'test-account-id',
85
+ createdAt: '2026-01-24T00:00:00Z',
86
+ updatedAt: '2026-01-24T00:00:00Z',
87
+ };
88
+ describe('hasXCredentials', () => {
89
+ it('returns false when no credentials file exists', () => {
90
+ expect(hasXCredentials(accountName)).toBe(false);
91
+ });
92
+ it('returns true when credentials file exists', () => {
93
+ saveXCredentials(accountName, validCreds);
94
+ expect(hasXCredentials(accountName)).toBe(true);
95
+ });
96
+ it('returns false for invalid account name', () => {
97
+ expect(hasXCredentials('../invalid')).toBe(false);
98
+ });
99
+ });
100
+ describe('saveXCredentials', () => {
101
+ it('saves credentials to disk', () => {
102
+ saveXCredentials(accountName, validCreds);
103
+ const credPath = getXCredentialsPath(accountName);
104
+ expect(fs.existsSync(credPath)).toBe(true);
105
+ });
106
+ it('creates directories if they do not exist', () => {
107
+ expect(fs.existsSync(getXCredentialsDir())).toBe(false);
108
+ saveXCredentials(accountName, validCreds);
109
+ expect(fs.existsSync(getXCredentialsDir())).toBe(true);
110
+ });
111
+ it('throws for invalid account name', () => {
112
+ expect(() => saveXCredentials('../invalid', validCreds)).toThrow();
113
+ });
114
+ });
115
+ describe('loadXCredentials', () => {
116
+ it('returns null when no credentials exist', () => {
117
+ expect(loadXCredentials(accountName)).toBeNull();
118
+ });
119
+ it('loads saved credentials', () => {
120
+ saveXCredentials(accountName, validCreds);
121
+ const loaded = loadXCredentials(accountName);
122
+ expect(loaded).toEqual(validCreds);
123
+ });
124
+ it('returns null for invalid JSON', () => {
125
+ ensureConfigDirs();
126
+ fs.writeFileSync(getXCredentialsPath(accountName), 'not valid json', { mode: 0o600 });
127
+ expect(loadXCredentials(accountName)).toBeNull();
128
+ });
129
+ it('returns null for invalid account name', () => {
130
+ expect(loadXCredentials('../invalid')).toBeNull();
131
+ });
132
+ });
133
+ describe('deleteXCredentials', () => {
134
+ it('removes credentials file', () => {
135
+ saveXCredentials(accountName, validCreds);
136
+ expect(hasXCredentials(accountName)).toBe(true);
137
+ deleteXCredentials(accountName);
138
+ expect(hasXCredentials(accountName)).toBe(false);
139
+ });
140
+ it('does not fail if file does not exist', () => {
141
+ expect(() => deleteXCredentials(accountName)).not.toThrow();
142
+ });
143
+ });
144
+ describe('listXAccounts', () => {
145
+ it('returns empty array when no accounts', () => {
146
+ expect(listXAccounts()).toEqual([]);
147
+ });
148
+ it('returns list of accounts', () => {
149
+ saveXCredentials('personal', validCreds);
150
+ saveXCredentials('work', { ...validCreds, screenName: 'workuser' });
151
+ const accounts = listXAccounts();
152
+ expect(accounts).toHaveLength(2);
153
+ expect(accounts.map(a => a.name)).toContain('personal');
154
+ expect(accounts.map(a => a.name)).toContain('work');
155
+ });
156
+ });
157
+ describe('validateXCredentials', () => {
158
+ it('returns true for valid credentials', () => {
159
+ expect(validateXCredentials(validCreds)).toBe(true);
160
+ });
161
+ it('returns false when apiKey is missing', () => {
162
+ expect(validateXCredentials({ ...validCreds, apiKey: '' })).toBe(false);
163
+ });
164
+ it('returns false when apiSecret is missing', () => {
165
+ expect(validateXCredentials({ ...validCreds, apiSecret: '' })).toBe(false);
166
+ });
167
+ it('returns false when accessToken is missing', () => {
168
+ expect(validateXCredentials({ ...validCreds, accessToken: '' })).toBe(false);
169
+ });
170
+ it('returns false when accessTokenSecret is missing', () => {
171
+ expect(validateXCredentials({ ...validCreds, accessTokenSecret: '' })).toBe(false);
172
+ });
173
+ });
174
+ });
175
+ describe('Project X account', () => {
176
+ beforeEach(() => {
177
+ // Create CLAUDE.md in test project
178
+ fs.writeFileSync(path.join(testProjectDir, 'CLAUDE.md'), '# Brand Guidelines\n\nSome content here.\n');
179
+ });
180
+ describe('getProjectXAccount', () => {
181
+ it('returns null when no X Account section', () => {
182
+ expect(getProjectXAccount(testProjectDir)).toBeNull();
183
+ });
184
+ it('returns account name when set', () => {
185
+ fs.writeFileSync(path.join(testProjectDir, 'CLAUDE.md'), '# Brand Guidelines\n\n## X Account\npersonal\n');
186
+ expect(getProjectXAccount(testProjectDir)).toBe('personal');
187
+ });
188
+ it('returns null for invalid account name', () => {
189
+ fs.writeFileSync(path.join(testProjectDir, 'CLAUDE.md'), '# Brand Guidelines\n\n## X Account\n../invalid\n');
190
+ expect(getProjectXAccount(testProjectDir)).toBeNull();
191
+ });
192
+ });
193
+ describe('setProjectXAccount', () => {
194
+ it('adds X Account section when missing', () => {
195
+ setProjectXAccount('personal', testProjectDir);
196
+ const content = fs.readFileSync(path.join(testProjectDir, 'CLAUDE.md'), 'utf-8');
197
+ expect(content).toContain('## X Account\npersonal');
198
+ });
199
+ it('updates existing X Account section', () => {
200
+ fs.writeFileSync(path.join(testProjectDir, 'CLAUDE.md'), '# Brand Guidelines\n\n## X Account\nold-account\n\n## Other\n');
201
+ setProjectXAccount('new-account', testProjectDir);
202
+ const content = fs.readFileSync(path.join(testProjectDir, 'CLAUDE.md'), 'utf-8');
203
+ expect(content).toContain('## X Account\nnew-account');
204
+ expect(content).not.toContain('old-account');
205
+ });
206
+ it('throws for invalid account name', () => {
207
+ expect(() => setProjectXAccount('../invalid', testProjectDir)).toThrow();
208
+ });
209
+ });
210
+ describe('removeProjectXAccount', () => {
211
+ it('removes X Account section', () => {
212
+ fs.writeFileSync(path.join(testProjectDir, 'CLAUDE.md'), '# Brand Guidelines\n\n## X Account\npersonal\n\n## Other\nContent\n');
213
+ removeProjectXAccount(testProjectDir);
214
+ const content = fs.readFileSync(path.join(testProjectDir, 'CLAUDE.md'), 'utf-8');
215
+ expect(content).not.toContain('## X Account');
216
+ expect(content).toContain('## Other');
217
+ });
218
+ it('does nothing when no X Account section', () => {
219
+ const original = fs.readFileSync(path.join(testProjectDir, 'CLAUDE.md'), 'utf-8');
220
+ removeProjectXAccount(testProjectDir);
221
+ const after = fs.readFileSync(path.join(testProjectDir, 'CLAUDE.md'), 'utf-8');
222
+ expect(after.trim()).toBe(original.trim());
223
+ });
224
+ });
225
+ });
226
+ });
@@ -108,13 +108,6 @@ export declare function updateSchedule(item: ContentItem, datetime: string): voi
108
108
  * @throws Error if move operation fails
109
109
  */
110
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
111
  /**
119
112
  * Add a published URL to a post
120
113
  * @param item - Content item to update
@@ -343,48 +343,6 @@ export function moveToPosts(item, cwd = process.cwd()) {
343
343
  throw new Error(`Failed to move ${item.path} to ${newPath}: ${error.message}`);
344
344
  }
345
345
  }
346
- /**
347
- * Move a post back to the drafts folder
348
- * @param item - Content item to move
349
- * @param cwd - Working directory
350
- * @throws Error if move operation fails
351
- */
352
- export function moveToDrafts(item, cwd = process.cwd()) {
353
- const { draftsDir } = getContentDirs(cwd);
354
- // Ensure drafts directory exists
355
- try {
356
- if (!fs.existsSync(draftsDir)) {
357
- fs.mkdirSync(draftsDir, { recursive: true });
358
- }
359
- }
360
- catch (error) {
361
- throw new Error(`Failed to create drafts directory: ${error.message}`);
362
- }
363
- // Add status field
364
- if (fs.existsSync(item.xFile)) {
365
- try {
366
- let content = fs.readFileSync(item.xFile, 'utf-8');
367
- if (!content.includes('## Status\n')) {
368
- content = content.replace(/## Media\n/, `## Status\nready\n\n## Media\n`);
369
- }
370
- else {
371
- content = content.replace(/## Status\n.+/, '## Status\nready');
372
- }
373
- fs.writeFileSync(item.xFile, content);
374
- }
375
- catch (error) {
376
- throw new Error(`Failed to update file ${item.xFile}: ${error.message}`);
377
- }
378
- }
379
- // Move to drafts
380
- const newPath = path.join(draftsDir, item.folder);
381
- try {
382
- fs.renameSync(item.path, newPath);
383
- }
384
- catch (error) {
385
- throw new Error(`Failed to move ${item.path} to ${newPath}: ${error.message}`);
386
- }
387
- }
388
346
  /**
389
347
  * Add a published URL to a post
390
348
  * @param item - Content item to update
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
- import { parseContent, findItem, createDraft, formatDate, sortBySchedule, isNoslopProject, getContentDirs, isValidUrl, updateStatus, updateSchedule, moveToPosts, moveToDrafts, addPublishedUrl, deleteDraft, } from './content.js';
5
+ import { parseContent, findItem, createDraft, formatDate, sortBySchedule, isNoslopProject, getContentDirs, isValidUrl, updateStatus, updateSchedule, moveToPosts, addPublishedUrl, deleteDraft, } from './content.js';
6
6
  describe('content.ts', () => {
7
7
  let testDir;
8
8
  beforeEach(() => {
@@ -390,59 +390,6 @@ Just content
390
390
  expect(fs.existsSync(path.join(testDir, 'posts', 'test-post'))).toBe(true);
391
391
  });
392
392
  });
393
- describe('moveToDrafts', () => {
394
- it('moves post folder to drafts', () => {
395
- const postsDir = path.join(testDir, 'posts');
396
- const draftsDir = path.join(testDir, 'drafts');
397
- const postDir = path.join(postsDir, 'test-post');
398
- fs.mkdirSync(postDir, { recursive: true });
399
- fs.mkdirSync(draftsDir, { recursive: true });
400
- const xFile = path.join(postDir, 'x.md');
401
- fs.writeFileSync(xFile, '# Test\n\n## Post\n```\ncontent\n```\n\n## Media\nNone');
402
- const item = {
403
- id: 'P001',
404
- folder: 'test-post',
405
- title: 'Test',
406
- post: 'content',
407
- published: '',
408
- media: 'None',
409
- postedAt: '',
410
- scheduledAt: '',
411
- status: 'draft',
412
- path: postDir,
413
- xFile,
414
- };
415
- moveToDrafts(item, testDir);
416
- expect(fs.existsSync(postDir)).toBe(false);
417
- expect(fs.existsSync(path.join(draftsDir, 'test-post'))).toBe(true);
418
- });
419
- it('adds status field to file', () => {
420
- const postsDir = path.join(testDir, 'posts');
421
- const draftsDir = path.join(testDir, 'drafts');
422
- const postDir = path.join(postsDir, 'test-post');
423
- fs.mkdirSync(postDir, { recursive: true });
424
- fs.mkdirSync(draftsDir, { recursive: true });
425
- const xFile = path.join(postDir, 'x.md');
426
- fs.writeFileSync(xFile, '# Test\n\n## Post\n```\ncontent\n```\n\n## Media\nNone');
427
- const item = {
428
- id: 'P001',
429
- folder: 'test-post',
430
- title: 'Test',
431
- post: 'content',
432
- published: '',
433
- media: 'None',
434
- postedAt: '',
435
- scheduledAt: '',
436
- status: 'draft',
437
- path: postDir,
438
- xFile,
439
- };
440
- moveToDrafts(item, testDir);
441
- const newXFile = path.join(draftsDir, 'test-post', 'x.md');
442
- const content = fs.readFileSync(newXFile, 'utf-8');
443
- expect(content).toContain('## Status\nready');
444
- });
445
- });
446
393
  describe('addPublishedUrl', () => {
447
394
  it('adds published URL to post', () => {
448
395
  const postsDir = path.join(testDir, 'posts');
@@ -8,12 +8,12 @@ export declare const POST_TEMPLATE = "# {{TITLE}}\n\n## Post\n```\n\n```\n\n## S
8
8
  * Template for NOSLOP.md - tool documentation
9
9
  * This file is regenerated on every launch to ensure documentation is up-to-date
10
10
  */
11
- export declare const NOSLOP_TEMPLATE = "# noslop - Content Workflow CLI\n\nA command-line tool for managing social media content with an interactive TUI.\n\n## Quick Start\n\n```bash\nnoslop # Open interactive TUI\nnoslop init # Initialize new project\nnoslop new \"My Post\" # Create new draft\n```\n\n## Commands\n\n### Core\n| Command | Description |\n|---------|-------------|\n| `noslop` | Open interactive TUI |\n| `noslop init` | Create folder structure + config files |\n| `noslop help` | Show all commands |\n\n### Content Management\n| Command | Description |\n|---------|-------------|\n| `noslop new <title>` | Create new draft with title |\n| `noslop list [--drafts|--posts]` | List content with schedule |\n| `noslop status` | Summary: counts, next scheduled |\n| `noslop show <id>` | Show full content of a post/draft |\n\n### Workflow Actions\n| Command | Description |\n|---------|-------------|\n| `noslop ready <id>` | Mark draft as ready to post |\n| `noslop unready <id>` | Mark draft as in-progress |\n| `noslop post <id>` | Move draft to posts folder |\n| `noslop unpost <id>` | Move post back to drafts |\n| `noslop publish <id> <url>` | Add published URL to post |\n| `noslop schedule <id> <datetime>` | Set/update scheduled time |\n| `noslop delete <id>` | Delete a draft |\n\n### Examples\n```bash\n# Full workflow\nnoslop new \"Monday Motivation\"\nnoslop schedule monday-motivation \"2026-01-27 09:00\"\nnoslop ready monday-motivation\n# ... post manually to platform ...\nnoslop post monday-motivation\nnoslop publish monday-motivation \"https://x.com/user/status/123\"\n```\n\n## Folder Structure\n\n```\nyour-project/\n\u251C\u2500\u2500 CLAUDE.md # Your brand voice & guidelines\n\u251C\u2500\u2500 NOSLOP.md # This file (tool documentation)\n\u251C\u2500\u2500 drafts/ # Work in progress\n\u2502 \u2514\u2500\u2500 post-name/\n\u2502 \u251C\u2500\u2500 x.md # Post content\n\u2502 \u2514\u2500\u2500 assets/ # Images, videos (optional)\n\u2514\u2500\u2500 posts/ # Published/scheduled content\n \u2514\u2500\u2500 post-name/\n \u251C\u2500\u2500 x.md\n \u2514\u2500\u2500 assets/\n```\n\n## Post File Format (x.md)\n\n```markdown\n# Post Title\n\n## Post\n\\`\\`\\`\nyour post content here\nlowercase, brand voice, etc.\n\\`\\`\\`\n\n## Status\ndraft | ready\n\n## Media\nDescription of media to create/attach\n\n## Scheduled\n2026-01-27 09:00\n\n## Posted\n2026-01-27 09:00\n\n## Published\nhttps://x.com/user/status/123\n```\n\n## TUI Keyboard Shortcuts\n\n| Key | Drafts Tab | Posts Tab |\n|-----|------------|-----------|\n| `Tab` | Switch to Posts | Switch to Drafts |\n| `\u2191/\u2193` | Navigate items | Navigate items |\n| `Enter` | Toggle ready/draft | Add URL |\n| `Space` | Move to Posts | Move to Drafts |\n| `Backspace` | Delete (drafts only) | \u2014 |\n| `s` | Toggle schedule view | Toggle schedule view |\n| `\u2190/\u2192` | Navigate weeks | Navigate weeks |\n| `q` | Quit | Quit |\n\n## Working with AI Assistants\n\nThis project is designed to work with Claude Code and other AI assistants:\n\n1. **Use CLI commands** instead of manual file editing\n2. **CLAUDE.md** contains your brand voice and guidelines\n3. **NOSLOP.md** (this file) documents all available commands\n\nThe AI should use commands like `noslop new`, `noslop ready`, `noslop post`\ninstead of directly creating/moving files.\n";
11
+ export declare const NOSLOP_TEMPLATE = "# noslop - Content Workflow CLI\n\nA command-line tool for managing social media content with an interactive TUI.\n\n## Quick Start\n\n```bash\nnoslop # Open interactive TUI\nnoslop init # Initialize new project\nnoslop new \"My Post\" # Create new draft\n```\n\n## Commands\n\n### Core\n| Command | Description |\n|---------|-------------|\n| `noslop` | Open interactive TUI |\n| `noslop init` | Create folder structure + config files |\n| `noslop help` | Show all commands |\n\n### Content Management\n| Command | Description |\n|---------|-------------|\n| `noslop new <title>` | Create new draft with title |\n| `noslop list [--drafts|--posts]` | List content with schedule |\n| `noslop status` | Summary: counts, next scheduled |\n| `noslop show <id>` | Show full content of a post/draft |\n\n### Workflow Actions\n| Command | Description |\n|---------|-------------|\n| `noslop ready <id>` | Mark draft as ready to post |\n| `noslop unready <id>` | Mark draft as in-progress |\n| `noslop schedule <id> <datetime>` | Set/update scheduled time |\n| `noslop delete <id>` | Delete a draft |\n\n### X Integration\n| Command | Description |\n|---------|-------------|\n| `noslop auth x` | Authenticate with X API |\n| `noslop auth x --status` | Check authentication status |\n| `noslop auth x --list` | List all authenticated accounts |\n| `noslop auth x --unlink` | Remove account link from project |\n| `noslop auth x --logout <name>` | Remove stored credentials |\n| `noslop x post <id>` | Post draft to X immediately |\n\n### Examples\n```bash\n# Full workflow\nnoslop new \"Monday Motivation\"\nnoslop ready monday-motivation\nnoslop x post monday-motivation # Posts to X, moves to posts/\n```\n\n## Folder Structure\n\n```\nyour-project/\n\u251C\u2500\u2500 CLAUDE.md # Your brand voice & guidelines\n\u251C\u2500\u2500 NOSLOP.md # This file (tool documentation)\n\u251C\u2500\u2500 drafts/ # Work in progress\n\u2502 \u2514\u2500\u2500 post-name/\n\u2502 \u251C\u2500\u2500 x.md # Post content\n\u2502 \u2514\u2500\u2500 assets/ # Images, videos (optional)\n\u2514\u2500\u2500 posts/ # Published/scheduled content\n \u2514\u2500\u2500 post-name/\n \u251C\u2500\u2500 x.md\n \u2514\u2500\u2500 assets/\n```\n\n## Post File Format (x.md)\n\n```markdown\n# Post Title\n\n## Post\n\\`\\`\\`\nyour post content here\nlowercase, brand voice, etc.\n\\`\\`\\`\n\n## Status\ndraft | ready\n\n## Media\nDescription of media to create/attach\n\n## Scheduled\n2026-01-27 09:00\n\n## Posted\n2026-01-27 09:00\n\n## Published\nhttps://x.com/user/status/123\n```\n\n## TUI Keyboard Shortcuts\n\n| Key | Drafts Tab | Posts Tab |\n|-----|------------|-----------|\n| `Tab` | Switch to Posts | Switch to Drafts |\n| `\u2191/\u2193` | Navigate items | Navigate items |\n| `Enter` | Toggle ready/draft | Add URL |\n| `Space` | Move to Posts | \u2014 |\n| `Backspace` | Delete (drafts only) | \u2014 |\n| `s` | Toggle schedule view | Toggle schedule view |\n| `\u2190/\u2192` | Navigate weeks | Navigate weeks |\n| `q` | Quit | Quit |\n\n## Working with AI Assistants\n\nThis project is designed to work with Claude Code and other AI assistants:\n\n1. **Use CLI commands** instead of manual file editing\n2. **CLAUDE.md** contains your brand voice and guidelines\n3. **NOSLOP.md** (this file) documents all available commands\n\nThe AI should use commands like `noslop new`, `noslop ready`, `noslop x post`\ninstead of directly creating/moving files.\n";
12
12
  /**
13
13
  * Template for CLAUDE.md - user-specific brand content
14
14
  * This file contains brand voice guidelines for AI assistants
15
15
  */
16
- export declare const CLAUDE_TEMPLATE = "# Content Guide\n\n> This project uses noslop for content management. See NOSLOP.md for CLI commands.\n\n---\n\n## Brand Voice\n\n<!--\nINSTRUCTIONS FOR AI: If this section is empty, interview the user to fill it in.\nAsk about tone, style, what to avoid, platform preferences, etc.\n-->\n\n### Tone & Style\n\n\n### Do's\n\n\n### Don'ts\n\n\n---\n\n## Platform Guidelines\n\n<!-- Which platforms? What's different about each? -->\n\n### Twitter/X\n\n\n### Other Platforms\n\n\n---\n\n## Key Messaging\n\n### Taglines\n\n\n### Key Phrases\n\n\n### Topics to Cover\n\n\n---\n\n## Content Examples\n\n<!-- Add example posts as you create them -->\n\n### Good Examples\n\n\n### Transformations (before \u2192 after)\n\n\n---\n\n## Visual Elements\n\n### Emoji Policy\n\n\n### ASCII Art Library\n\n\n";
16
+ export declare const CLAUDE_TEMPLATE = "# Content Guide\n\n> This project uses noslop for content management. See NOSLOP.md for CLI commands.\n\n---\n\n## Brand Voice\n\n<!--\nINSTRUCTIONS FOR AI: If this section is empty, interview the user to fill it in.\nAsk about tone, style, what to avoid, platform preferences, etc.\n-->\n\n### Tone & Style\n\n\n### Do's\n\n\n### Don'ts\n\n\n---\n\n## Platform Guidelines\n\n<!-- Which platforms? What's different about each? -->\n\n### Twitter/X\n\n<!-- Run `noslop auth x` to connect your X account -->\n\n\n### Other Platforms\n\n\n---\n\n## Key Messaging\n\n### Taglines\n\n\n### Key Phrases\n\n\n### Topics to Cover\n\n\n---\n\n## Content Examples\n\n<!-- Add example posts as you create them -->\n\n### Good Examples\n\n\n### Transformations (before \u2192 after)\n\n\n---\n\n## Visual Elements\n\n### Emoji Policy\n\n\n### ASCII Art Library\n\n\n";
17
17
  /**
18
18
  * Ensure NOSLOP.md documentation file exists and is up-to-date
19
19
  * Creates or overwrites NOSLOP.md in the specified directory with
@@ -62,21 +62,25 @@ noslop new "My Post" # Create new draft
62
62
  |---------|-------------|
63
63
  | \`noslop ready <id>\` | Mark draft as ready to post |
64
64
  | \`noslop unready <id>\` | Mark draft as in-progress |
65
- | \`noslop post <id>\` | Move draft to posts folder |
66
- | \`noslop unpost <id>\` | Move post back to drafts |
67
- | \`noslop publish <id> <url>\` | Add published URL to post |
68
65
  | \`noslop schedule <id> <datetime>\` | Set/update scheduled time |
69
66
  | \`noslop delete <id>\` | Delete a draft |
70
67
 
68
+ ### X Integration
69
+ | Command | Description |
70
+ |---------|-------------|
71
+ | \`noslop auth x\` | Authenticate with X API |
72
+ | \`noslop auth x --status\` | Check authentication status |
73
+ | \`noslop auth x --list\` | List all authenticated accounts |
74
+ | \`noslop auth x --unlink\` | Remove account link from project |
75
+ | \`noslop auth x --logout <name>\` | Remove stored credentials |
76
+ | \`noslop x post <id>\` | Post draft to X immediately |
77
+
71
78
  ### Examples
72
79
  \`\`\`bash
73
80
  # Full workflow
74
81
  noslop new "Monday Motivation"
75
- noslop schedule monday-motivation "2026-01-27 09:00"
76
82
  noslop ready monday-motivation
77
- # ... post manually to platform ...
78
- noslop post monday-motivation
79
- noslop publish monday-motivation "https://x.com/user/status/123"
83
+ noslop x post monday-motivation # Posts to X, moves to posts/
80
84
  \`\`\`
81
85
 
82
86
  ## Folder Structure
@@ -129,7 +133,7 @@ https://x.com/user/status/123
129
133
  | \`Tab\` | Switch to Posts | Switch to Drafts |
130
134
  | \`↑/↓\` | Navigate items | Navigate items |
131
135
  | \`Enter\` | Toggle ready/draft | Add URL |
132
- | \`Space\` | Move to Posts | Move to Drafts |
136
+ | \`Space\` | Move to Posts | |
133
137
  | \`Backspace\` | Delete (drafts only) | — |
134
138
  | \`s\` | Toggle schedule view | Toggle schedule view |
135
139
  | \`←/→\` | Navigate weeks | Navigate weeks |
@@ -143,7 +147,7 @@ This project is designed to work with Claude Code and other AI assistants:
143
147
  2. **CLAUDE.md** contains your brand voice and guidelines
144
148
  3. **NOSLOP.md** (this file) documents all available commands
145
149
 
146
- The AI should use commands like \`noslop new\`, \`noslop ready\`, \`noslop post\`
150
+ The AI should use commands like \`noslop new\`, \`noslop ready\`, \`noslop x post\`
147
151
  instead of directly creating/moving files.
148
152
  `;
149
153
  /**
@@ -180,6 +184,8 @@ Ask about tone, style, what to avoid, platform preferences, etc.
180
184
 
181
185
  ### Twitter/X
182
186
 
187
+ <!-- Run \`noslop auth x\` to connect your X account -->
188
+
183
189
 
184
190
  ### Other Platforms
185
191