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.
- package/README.md +84 -4
- package/dist/index.js +321 -48
- package/dist/lib/config.d.ts +76 -0
- package/dist/lib/config.js +188 -0
- package/dist/lib/config.test.d.ts +1 -0
- package/dist/lib/config.test.js +226 -0
- package/dist/lib/content.d.ts +0 -7
- package/dist/lib/content.js +0 -42
- package/dist/lib/content.test.js +1 -54
- package/dist/lib/templates.d.ts +2 -2
- package/dist/lib/templates.js +15 -9
- package/dist/lib/x-api.d.ts +49 -0
- package/dist/lib/x-api.js +208 -0
- package/dist/lib/x-api.test.d.ts +1 -0
- package/dist/lib/x-api.test.js +23 -0
- package/dist/tui.js +9 -16
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -74,12 +74,20 @@ noslop
|
|
|
74
74
|
|---------|-------------|
|
|
75
75
|
| `noslop ready <id>` | Mark draft as ready |
|
|
76
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
77
|
| `noslop schedule <id> <datetime>` | Set schedule (YYYY-MM-DD HH:MM) |
|
|
81
78
|
| `noslop delete <id>` | Delete a draft |
|
|
82
79
|
|
|
80
|
+
### X Integration
|
|
81
|
+
|
|
82
|
+
| Command | Description |
|
|
83
|
+
|---------|-------------|
|
|
84
|
+
| `noslop auth x` | Authenticate with X API |
|
|
85
|
+
| `noslop auth x --status` | Check authentication status |
|
|
86
|
+
| `noslop auth x --list` | List all authenticated accounts |
|
|
87
|
+
| `noslop auth x --unlink` | Remove account link from project |
|
|
88
|
+
| `noslop auth x --logout <name>` | Remove stored credentials |
|
|
89
|
+
| `noslop x post <id>` | Post a draft to X immediately |
|
|
90
|
+
|
|
83
91
|
## TUI Keyboard Shortcuts
|
|
84
92
|
|
|
85
93
|
| Key | Action |
|
|
@@ -87,7 +95,7 @@ noslop
|
|
|
87
95
|
| `Tab` | Switch between Drafts/Posts |
|
|
88
96
|
| `↑/↓` | Navigate items |
|
|
89
97
|
| `Enter` | Toggle ready (drafts) / Add URL (posts) |
|
|
90
|
-
| `Space` | Move to Posts
|
|
98
|
+
| `Space` | Move to Posts |
|
|
91
99
|
| `Backspace` | Delete draft (in-progress only) |
|
|
92
100
|
| `s` | Toggle schedule view |
|
|
93
101
|
| `←/→` | Navigate weeks (schedule view) |
|
|
@@ -138,6 +146,78 @@ Description of media to attach
|
|
|
138
146
|
https://x.com/user/status/123
|
|
139
147
|
```
|
|
140
148
|
|
|
149
|
+
The `## Published` URL is automatically added when you use `noslop x post`.
|
|
150
|
+
|
|
151
|
+
## X Integration
|
|
152
|
+
|
|
153
|
+
noslop can post directly to X (Twitter).
|
|
154
|
+
|
|
155
|
+
### Setup
|
|
156
|
+
|
|
157
|
+
1. Create an X Developer App at [X Developer Portal](https://developer.x.com/en/portal/dashboard)
|
|
158
|
+
- Enable "Read and Write" permissions
|
|
159
|
+
- Note your API Key and API Secret
|
|
160
|
+
|
|
161
|
+
2. Authenticate (in your noslop project folder):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
noslop auth x
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
3. Follow the prompts:
|
|
168
|
+
- Enter an account name (e.g., "personal", "work", "client-acme")
|
|
169
|
+
- Enter your API Key and Secret (or set `NOSLOP_X_API_KEY` and `NOSLOP_X_API_SECRET`)
|
|
170
|
+
- Browser opens to X for authorization
|
|
171
|
+
- Click "Authorize" on X
|
|
172
|
+
- Enter the PIN shown by X
|
|
173
|
+
|
|
174
|
+
The account name is saved in your project's `CLAUDE.md` file:
|
|
175
|
+
```markdown
|
|
176
|
+
## X Account
|
|
177
|
+
personal
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Credentials are stored globally at `~/.config/noslop/credentials/x/`.
|
|
181
|
+
|
|
182
|
+
### Multiple Accounts
|
|
183
|
+
|
|
184
|
+
Each noslop project can be linked to a different X account:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# In project A (your personal account)
|
|
188
|
+
noslop auth x # Enter: personal
|
|
189
|
+
|
|
190
|
+
# In project B (client work)
|
|
191
|
+
noslop auth x # Enter: client-acme
|
|
192
|
+
|
|
193
|
+
# List all authenticated accounts
|
|
194
|
+
noslop auth x --list
|
|
195
|
+
|
|
196
|
+
# Check which account this project uses
|
|
197
|
+
noslop auth x --status
|
|
198
|
+
|
|
199
|
+
# Unlink project from account
|
|
200
|
+
noslop auth x --unlink
|
|
201
|
+
|
|
202
|
+
# Remove stored credentials
|
|
203
|
+
noslop auth x --logout personal
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Workflow
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# 1. Create a draft
|
|
210
|
+
noslop new "Monday Motivation"
|
|
211
|
+
|
|
212
|
+
# 2. Mark it ready when done
|
|
213
|
+
noslop ready D001
|
|
214
|
+
|
|
215
|
+
# 3. Post to X
|
|
216
|
+
noslop x post D001
|
|
217
|
+
|
|
218
|
+
# Draft is automatically moved to posts/ with the X URL saved
|
|
219
|
+
```
|
|
220
|
+
|
|
141
221
|
## Working with AI Assistants
|
|
142
222
|
|
|
143
223
|
noslop is designed to work with Claude Code and other AI assistants:
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { isNoslopProject, getAllContent, findItem, createDraft, updateStatus, updateSchedule, moveToPosts,
|
|
3
|
+
import { isNoslopProject, getAllContent, findItem, createDraft, updateStatus, updateSchedule, moveToPosts, addPublishedUrl, deleteDraft, sortBySchedule, } from './lib/content.js';
|
|
4
4
|
import { initProject, ensureNoslopMd } from './lib/templates.js';
|
|
5
|
+
import { hasXCredentials, loadXCredentials, saveXCredentials, deleteXCredentials, validateXCredentials, getProjectXAccount, setProjectXAccount, removeProjectXAccount, listXAccounts, isValidAccountName, } from './lib/config.js';
|
|
6
|
+
import { postTweet, verifyCredentials, getOAuthRequestToken, getOAuthAccessToken, } from './lib/x-api.js';
|
|
7
|
+
import readline from 'readline';
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
/**
|
|
10
|
+
* Prompt user for input
|
|
11
|
+
*/
|
|
12
|
+
function prompt(question) {
|
|
13
|
+
const rl = readline.createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
});
|
|
17
|
+
return new Promise(resolve => {
|
|
18
|
+
rl.question(question, answer => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve(answer.trim());
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Open URL in default browser
|
|
26
|
+
*/
|
|
27
|
+
function openBrowser(url) {
|
|
28
|
+
let command;
|
|
29
|
+
if (process.platform === 'darwin') {
|
|
30
|
+
command = `open "${url}"`;
|
|
31
|
+
}
|
|
32
|
+
else if (process.platform === 'win32') {
|
|
33
|
+
command = `start "${url}"`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
command = `xdg-open "${url}"`;
|
|
37
|
+
}
|
|
38
|
+
exec(command, err => {
|
|
39
|
+
if (err) {
|
|
40
|
+
console.log(`Could not open browser. Please visit: ${url}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
5
44
|
const program = new Command();
|
|
6
45
|
program
|
|
7
46
|
.name('noslop')
|
|
@@ -185,12 +224,13 @@ program
|
|
|
185
224
|
updateStatus(item, 'draft');
|
|
186
225
|
console.log(`Marked as in-progress: ${item.folder}`);
|
|
187
226
|
});
|
|
188
|
-
//
|
|
227
|
+
// Schedule command
|
|
189
228
|
program
|
|
190
|
-
.command('
|
|
191
|
-
.description('
|
|
229
|
+
.command('schedule')
|
|
230
|
+
.description('Set/update scheduled time')
|
|
192
231
|
.argument('<id>', 'Folder name or ID')
|
|
193
|
-
.
|
|
232
|
+
.argument('<datetime>', 'Date and time (YYYY-MM-DD HH:MM)')
|
|
233
|
+
.action((id, datetime) => {
|
|
194
234
|
if (!isNoslopProject()) {
|
|
195
235
|
console.log('Not a noslop project. Run `noslop init` first.');
|
|
196
236
|
process.exit(1);
|
|
@@ -200,13 +240,13 @@ program
|
|
|
200
240
|
console.log(`Not found: ${id}`);
|
|
201
241
|
process.exit(1);
|
|
202
242
|
}
|
|
203
|
-
|
|
204
|
-
console.log(`
|
|
243
|
+
updateSchedule(item, datetime);
|
|
244
|
+
console.log(`Scheduled: ${item.folder} @ ${datetime}`);
|
|
205
245
|
});
|
|
206
|
-
//
|
|
246
|
+
// Delete command
|
|
207
247
|
program
|
|
208
|
-
.command('
|
|
209
|
-
.description('
|
|
248
|
+
.command('delete')
|
|
249
|
+
.description('Delete a draft')
|
|
210
250
|
.argument('<id>', 'Folder name or ID')
|
|
211
251
|
.action((id) => {
|
|
212
252
|
if (!isNoslopProject()) {
|
|
@@ -218,64 +258,297 @@ program
|
|
|
218
258
|
console.log(`Not found: ${id}`);
|
|
219
259
|
process.exit(1);
|
|
220
260
|
}
|
|
221
|
-
|
|
222
|
-
console.log(`
|
|
261
|
+
deleteDraft(item);
|
|
262
|
+
console.log(`Deleted: ${item.folder}`);
|
|
223
263
|
});
|
|
224
|
-
//
|
|
225
|
-
program
|
|
226
|
-
|
|
227
|
-
.
|
|
228
|
-
.
|
|
229
|
-
.
|
|
230
|
-
.
|
|
264
|
+
// Auth command group
|
|
265
|
+
const auth = program.command('auth').description('Manage platform authentication');
|
|
266
|
+
auth
|
|
267
|
+
.command('x')
|
|
268
|
+
.description('Authenticate with X (Twitter)')
|
|
269
|
+
.option('--status', 'Check authentication status')
|
|
270
|
+
.option('--list', 'List all authenticated accounts')
|
|
271
|
+
.option('--unlink', 'Remove account link from this project')
|
|
272
|
+
.option('--logout <account>', 'Remove stored credentials for account')
|
|
273
|
+
.action(async (options) => {
|
|
274
|
+
// --logout: Remove credentials for a specific account
|
|
275
|
+
if (options.logout) {
|
|
276
|
+
if (!hasXCredentials(options.logout)) {
|
|
277
|
+
console.log(`Account "${options.logout}" not found.`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
deleteXCredentials(options.logout);
|
|
281
|
+
console.log(`Credentials removed for account "${options.logout}".`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// --list: Show all authenticated accounts
|
|
285
|
+
if (options.list) {
|
|
286
|
+
const accounts = listXAccounts();
|
|
287
|
+
if (accounts.length === 0) {
|
|
288
|
+
console.log('No authenticated X accounts.');
|
|
289
|
+
console.log('Run `noslop auth x` to authenticate.');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
console.log('Authenticated X accounts:');
|
|
293
|
+
for (const acc of accounts) {
|
|
294
|
+
const handle = acc.screenName ? ` (@${acc.screenName})` : '';
|
|
295
|
+
console.log(` ${acc.name}${handle}`);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// --unlink: Remove account link from project
|
|
300
|
+
if (options.unlink) {
|
|
301
|
+
if (!isNoslopProject()) {
|
|
302
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
const current = getProjectXAccount();
|
|
306
|
+
if (!current) {
|
|
307
|
+
console.log('This project is not linked to an X account.');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
removeProjectXAccount();
|
|
311
|
+
console.log(`Unlinked project from account "${current}".`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// --status: Show current account status
|
|
315
|
+
if (options.status) {
|
|
316
|
+
if (!isNoslopProject()) {
|
|
317
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
const accountName = getProjectXAccount();
|
|
321
|
+
if (!accountName) {
|
|
322
|
+
console.log('This project is not linked to an X account.');
|
|
323
|
+
console.log('Run `noslop auth x` to link an account.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!hasXCredentials(accountName)) {
|
|
327
|
+
console.log(`Project linked to "${accountName}" but credentials not found.`);
|
|
328
|
+
console.log('Run `noslop auth x` to re-authenticate.');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const creds = loadXCredentials(accountName);
|
|
332
|
+
if (!creds || !validateXCredentials(creds)) {
|
|
333
|
+
console.log(`Credentials for "${accountName}" are invalid.`);
|
|
334
|
+
console.log('Run `noslop auth x` to re-authenticate.');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
console.log(`Account: ${accountName}`);
|
|
338
|
+
console.log(` Handle: @${creds.screenName || '(unknown)'}`);
|
|
339
|
+
console.log(` Since: ${creds.createdAt}`);
|
|
340
|
+
// Verify credentials work
|
|
341
|
+
const user = await verifyCredentials(creds);
|
|
342
|
+
console.log(` Status: ${user ? 'connected' : 'not connected'}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Default: Link project to account (with authentication if needed)
|
|
231
346
|
if (!isNoslopProject()) {
|
|
232
347
|
console.log('Not a noslop project. Run `noslop init` first.');
|
|
233
348
|
process.exit(1);
|
|
234
349
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
350
|
+
console.log('X API Authentication');
|
|
351
|
+
console.log('====================');
|
|
352
|
+
console.log('');
|
|
353
|
+
// Check if project already has an account
|
|
354
|
+
const existingAccount = getProjectXAccount();
|
|
355
|
+
if (existingAccount) {
|
|
356
|
+
console.log(`This project is linked to account "${existingAccount}".`);
|
|
357
|
+
const change = await prompt('Change account? [y/N]: ');
|
|
358
|
+
if (change.toLowerCase() !== 'y') {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
console.log('');
|
|
362
|
+
}
|
|
363
|
+
// Ask for account name
|
|
364
|
+
const accountName = await prompt('Account name for this project (e.g., personal, work): ');
|
|
365
|
+
if (!accountName) {
|
|
366
|
+
console.log('Account name is required.');
|
|
238
367
|
process.exit(1);
|
|
239
368
|
}
|
|
240
|
-
|
|
241
|
-
|
|
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.');
|
|
369
|
+
if (!isValidAccountName(accountName)) {
|
|
370
|
+
console.log('Invalid account name. Use only letters, numbers, dashes, underscores.');
|
|
253
371
|
process.exit(1);
|
|
254
372
|
}
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
373
|
+
// Check if account already exists
|
|
374
|
+
if (hasXCredentials(accountName)) {
|
|
375
|
+
const creds = loadXCredentials(accountName);
|
|
376
|
+
const handle = creds?.screenName ? ` (@${creds.screenName})` : '';
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log(`Account "${accountName}"${handle} already exists.`);
|
|
379
|
+
const useExisting = await prompt('Use this account? [Y/n]: ');
|
|
380
|
+
if (useExisting.toLowerCase() !== 'n') {
|
|
381
|
+
setProjectXAccount(accountName);
|
|
382
|
+
console.log('');
|
|
383
|
+
console.log(`Project linked to account "${accountName}".`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
console.log('');
|
|
387
|
+
console.log('Starting new authentication...');
|
|
388
|
+
}
|
|
389
|
+
// Get API key and secret
|
|
390
|
+
let apiKey = process.env.NOSLOP_X_API_KEY;
|
|
391
|
+
let apiSecret = process.env.NOSLOP_X_API_SECRET;
|
|
392
|
+
if (!apiKey || !apiSecret) {
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log('You need an X Developer App. Create one at:');
|
|
395
|
+
console.log(' https://developer.x.com/en/portal/dashboard');
|
|
396
|
+
console.log('');
|
|
397
|
+
console.log('Make sure to enable "Read and Write" permissions.');
|
|
398
|
+
console.log('');
|
|
399
|
+
apiKey = await prompt('Enter API Key (Consumer Key): ');
|
|
400
|
+
apiSecret = await prompt('Enter API Secret (Consumer Secret): ');
|
|
401
|
+
if (!apiKey || !apiSecret) {
|
|
402
|
+
console.log('API Key and Secret are required.');
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log('');
|
|
408
|
+
console.log('Using API credentials from environment variables.');
|
|
409
|
+
}
|
|
410
|
+
// Get request token
|
|
411
|
+
console.log('');
|
|
412
|
+
console.log('Starting authorization...');
|
|
413
|
+
let requestToken;
|
|
414
|
+
try {
|
|
415
|
+
requestToken = await getOAuthRequestToken(apiKey, apiSecret);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
console.log(`Failed to start authorization: ${error.message}`);
|
|
419
|
+
console.log('');
|
|
420
|
+
console.log('Check that your API Key and Secret are correct.');
|
|
258
421
|
process.exit(1);
|
|
259
422
|
}
|
|
260
|
-
|
|
261
|
-
console.log(
|
|
423
|
+
// Open browser for user authorization
|
|
424
|
+
console.log('');
|
|
425
|
+
console.log('Opening browser for authorization...');
|
|
426
|
+
console.log('');
|
|
427
|
+
console.log('If browser does not open, visit:');
|
|
428
|
+
console.log(` ${requestToken.authorizeUrl}`);
|
|
429
|
+
console.log('');
|
|
430
|
+
openBrowser(requestToken.authorizeUrl);
|
|
431
|
+
// Get PIN from user
|
|
432
|
+
const pin = await prompt('Enter the PIN from X: ');
|
|
433
|
+
if (!pin) {
|
|
434
|
+
console.log('PIN is required.');
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
// Exchange PIN for access tokens
|
|
438
|
+
console.log('');
|
|
439
|
+
console.log('Completing authorization...');
|
|
440
|
+
let accessTokenResponse;
|
|
441
|
+
try {
|
|
442
|
+
accessTokenResponse = await getOAuthAccessToken(apiKey, apiSecret, requestToken.oauthToken, requestToken.oauthTokenSecret, pin);
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
console.log(`Authorization failed: ${error.message}`);
|
|
446
|
+
console.log('');
|
|
447
|
+
console.log('Make sure you entered the correct PIN.');
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
// Create credentials
|
|
451
|
+
const credentials = {
|
|
452
|
+
apiKey,
|
|
453
|
+
apiSecret,
|
|
454
|
+
accessToken: accessTokenResponse.accessToken,
|
|
455
|
+
accessTokenSecret: accessTokenResponse.accessTokenSecret,
|
|
456
|
+
screenName: accessTokenResponse.screenName,
|
|
457
|
+
createdAt: new Date().toISOString(),
|
|
458
|
+
updatedAt: new Date().toISOString(),
|
|
459
|
+
};
|
|
460
|
+
console.log('');
|
|
461
|
+
console.log(`Authorized as @${accessTokenResponse.screenName}`);
|
|
462
|
+
// Verify credentials work
|
|
463
|
+
const user = await verifyCredentials(credentials);
|
|
464
|
+
if (!user) {
|
|
465
|
+
saveXCredentials(accountName, credentials);
|
|
466
|
+
setProjectXAccount(accountName);
|
|
467
|
+
console.log('');
|
|
468
|
+
console.log('Credentials saved but verification failed.');
|
|
469
|
+
console.log('You may need to check your API permissions.');
|
|
470
|
+
console.log('');
|
|
471
|
+
console.log(`Project linked to account "${accountName}".`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
saveXCredentials(accountName, credentials);
|
|
475
|
+
setProjectXAccount(accountName);
|
|
476
|
+
console.log('');
|
|
477
|
+
console.log('Authentication successful!');
|
|
478
|
+
console.log(` Account: ${accountName} (@${user.username})`);
|
|
479
|
+
console.log('');
|
|
480
|
+
console.log(`Project linked to account "${accountName}".`);
|
|
481
|
+
console.log('');
|
|
482
|
+
console.log('You can now post to X with: noslop x post <id>');
|
|
262
483
|
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.action((id) => {
|
|
484
|
+
/**
|
|
485
|
+
* Get X credentials for the current project
|
|
486
|
+
* Exits with error if not authenticated
|
|
487
|
+
*/
|
|
488
|
+
function getProjectCredentials() {
|
|
269
489
|
if (!isNoslopProject()) {
|
|
270
490
|
console.log('Not a noslop project. Run `noslop init` first.');
|
|
271
491
|
process.exit(1);
|
|
272
492
|
}
|
|
493
|
+
const accountName = getProjectXAccount();
|
|
494
|
+
if (!accountName) {
|
|
495
|
+
console.log('This project is not linked to an X account.');
|
|
496
|
+
console.log('Run `noslop auth x` to link an account.');
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
if (!hasXCredentials(accountName)) {
|
|
500
|
+
console.log(`Account "${accountName}" not found.`);
|
|
501
|
+
console.log('Run `noslop auth x` to re-authenticate.');
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
const creds = loadXCredentials(accountName);
|
|
505
|
+
if (!creds) {
|
|
506
|
+
console.log(`Failed to load credentials for "${accountName}".`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
return creds;
|
|
510
|
+
}
|
|
511
|
+
// X command group
|
|
512
|
+
const x = program.command('x').description('X (Twitter) integration commands');
|
|
513
|
+
x.command('post')
|
|
514
|
+
.description('Post a draft to X immediately')
|
|
515
|
+
.argument('<id>', 'Draft folder name or ID')
|
|
516
|
+
.action(async (id) => {
|
|
517
|
+
const creds = getProjectCredentials();
|
|
518
|
+
// Find the item
|
|
273
519
|
const item = findItem(id);
|
|
274
520
|
if (!item) {
|
|
275
521
|
console.log(`Not found: ${id}`);
|
|
276
522
|
process.exit(1);
|
|
277
523
|
}
|
|
278
|
-
|
|
279
|
-
|
|
524
|
+
// Check if already posted
|
|
525
|
+
if (item.published?.startsWith('http')) {
|
|
526
|
+
console.log(`Already published: ${item.folder}`);
|
|
527
|
+
console.log(` URL: ${item.published}`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
// Validate content
|
|
531
|
+
if (!item.post || item.post.trim() === '') {
|
|
532
|
+
console.log(`Empty post content: ${item.folder}`);
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
// Post to X
|
|
536
|
+
console.log(`Posting to X: ${item.folder}`);
|
|
537
|
+
console.log('');
|
|
538
|
+
try {
|
|
539
|
+
const result = await postTweet(item.post, creds);
|
|
540
|
+
// Add published URL
|
|
541
|
+
addPublishedUrl(item, result.url);
|
|
542
|
+
// Move to posts folder if in drafts
|
|
543
|
+
if (item.path.includes('/drafts/')) {
|
|
544
|
+
moveToPosts(item);
|
|
545
|
+
}
|
|
546
|
+
console.log('Posted successfully!');
|
|
547
|
+
console.log(` URL: ${result.url}`);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
console.log(`Failed to post: ${error.message}`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
280
553
|
});
|
|
281
554
|
program.parse();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the noslop config directory path
|
|
3
|
+
* Uses ~/.config/noslop following XDG Base Directory spec
|
|
4
|
+
*/
|
|
5
|
+
export declare function getConfigDir(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Get the X credentials directory path
|
|
8
|
+
*/
|
|
9
|
+
export declare function getXCredentialsDir(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Ensure config directories exist with proper permissions
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureConfigDirs(): void;
|
|
14
|
+
/**
|
|
15
|
+
* X API credentials structure
|
|
16
|
+
*/
|
|
17
|
+
export interface XCredentials {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
apiSecret: string;
|
|
20
|
+
accessToken: string;
|
|
21
|
+
accessTokenSecret: string;
|
|
22
|
+
screenName?: string;
|
|
23
|
+
accountId?: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate account name (alphanumeric, dashes, underscores)
|
|
29
|
+
*/
|
|
30
|
+
export declare function isValidAccountName(name: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Get the path to X credentials file for an account
|
|
33
|
+
*/
|
|
34
|
+
export declare function getXCredentialsPath(accountName: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Check if X credentials exist for an account
|
|
37
|
+
*/
|
|
38
|
+
export declare function hasXCredentials(accountName: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Load X credentials from disk for an account
|
|
41
|
+
* @returns Credentials object or null if not found
|
|
42
|
+
*/
|
|
43
|
+
export declare function loadXCredentials(accountName: string): XCredentials | null;
|
|
44
|
+
/**
|
|
45
|
+
* Save X credentials to disk with restricted permissions
|
|
46
|
+
* @param accountName - Account name to save under
|
|
47
|
+
* @param credentials - Credentials to save
|
|
48
|
+
*/
|
|
49
|
+
export declare function saveXCredentials(accountName: string, credentials: XCredentials): void;
|
|
50
|
+
/**
|
|
51
|
+
* Delete X credentials from disk
|
|
52
|
+
*/
|
|
53
|
+
export declare function deleteXCredentials(accountName: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* List all X accounts with stored credentials
|
|
56
|
+
*/
|
|
57
|
+
export declare function listXAccounts(): Array<{
|
|
58
|
+
name: string;
|
|
59
|
+
screenName?: string;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Validate that credentials have all required fields
|
|
63
|
+
*/
|
|
64
|
+
export declare function validateXCredentials(creds: XCredentials): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Get the X account name from project's CLAUDE.md
|
|
67
|
+
*/
|
|
68
|
+
export declare function getProjectXAccount(cwd?: string): string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Set the X account name in project's CLAUDE.md
|
|
71
|
+
*/
|
|
72
|
+
export declare function setProjectXAccount(accountName: string, cwd?: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Remove the X account from project's CLAUDE.md
|
|
75
|
+
*/
|
|
76
|
+
export declare function removeProjectXAccount(cwd?: string): void;
|