@hidden-leaf/x-skill 1.0.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/.env.example +50 -0
- package/CLAUDE.snippet.md +34 -0
- package/README.md +220 -0
- package/SKILL.md +265 -0
- package/dist/cache/store.d.ts +48 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +381 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/clients/types.d.ts +217 -0
- package/dist/clients/types.d.ts.map +1 -0
- package/dist/clients/types.js +77 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/clients/x-client.d.ts +111 -0
- package/dist/clients/x-client.d.ts.map +1 -0
- package/dist/clients/x-client.js +421 -0
- package/dist/clients/x-client.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/skills/bookmarks/index.d.ts +79 -0
- package/dist/skills/bookmarks/index.d.ts.map +1 -0
- package/dist/skills/bookmarks/index.js +288 -0
- package/dist/skills/bookmarks/index.js.map +1 -0
- package/dist/skills/bookmarks/synthesize.d.ts +40 -0
- package/dist/skills/bookmarks/synthesize.d.ts.map +1 -0
- package/dist/skills/bookmarks/synthesize.js +164 -0
- package/dist/skills/bookmarks/synthesize.js.map +1 -0
- package/dist/skills/bookmarks/types.d.ts +71 -0
- package/dist/skills/bookmarks/types.d.ts.map +1 -0
- package/dist/skills/bookmarks/types.js +6 -0
- package/dist/skills/bookmarks/types.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +43 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +68 -0
- package/scripts/cache-report.ts +25 -0
- package/scripts/create-atlsk-ticket.ts +68 -0
- package/scripts/create-deep-dive-ticket.ts +66 -0
- package/scripts/create-jira-project.ts +31 -0
- package/scripts/create-launch-and-roadmap.ts +200 -0
- package/scripts/create-next-session.ts +71 -0
- package/scripts/create-roadmap.ts +150 -0
- package/scripts/debug-api.ts +45 -0
- package/scripts/debug-folder.ts +37 -0
- package/scripts/jira-close-v1-tickets.ts +83 -0
- package/scripts/jira-v1-close-and-post-v1.ts +272 -0
- package/scripts/oauth-flow.ts +216 -0
- package/scripts/postinstall.js +112 -0
- package/scripts/sync-test.ts +36 -0
- package/scripts/test-refresh-forced.ts +29 -0
- package/scripts/test-refresh.ts +25 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Code Flow with PKCE for X API.
|
|
3
|
+
*
|
|
4
|
+
* Run once to get your User Access Token:
|
|
5
|
+
* npx tsx scripts/oauth-flow.ts
|
|
6
|
+
*
|
|
7
|
+
* This will:
|
|
8
|
+
* 1. Generate a PKCE code challenge
|
|
9
|
+
* 2. Open your browser to X's authorize page
|
|
10
|
+
* 3. Start a local server to catch the callback
|
|
11
|
+
* 4. Exchange the auth code for a User Access Token
|
|
12
|
+
* 5. Print the token + user ID to paste into your .env
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as http from 'http';
|
|
16
|
+
import * as crypto from 'crypto';
|
|
17
|
+
import * as url from 'url';
|
|
18
|
+
import * as dotenv from 'dotenv';
|
|
19
|
+
|
|
20
|
+
dotenv.config();
|
|
21
|
+
|
|
22
|
+
const CLIENT_ID = process.env.X_CONSUMER_KEY;
|
|
23
|
+
const CLIENT_SECRET = process.env.X_CONSUMER_SECRET;
|
|
24
|
+
const CALLBACK_URL = process.env.X_CALLBACK_URL || 'http://localhost:3000/callback';
|
|
25
|
+
const SCOPES = ['bookmark.read', 'tweet.read', 'users.read', 'offline.access'];
|
|
26
|
+
|
|
27
|
+
if (!CLIENT_ID || !CLIENT_SECRET) {
|
|
28
|
+
console.error('ERROR: X_CONSUMER_KEY and X_CONSUMER_SECRET must be set in .env');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// PKCE helpers
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
function generateCodeVerifier(): string {
|
|
37
|
+
return crypto.randomBytes(32).toString('base64url');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generateCodeChallenge(verifier: string): string {
|
|
41
|
+
return crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generateState(): string {
|
|
45
|
+
return crypto.randomBytes(16).toString('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Token exchange
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
async function exchangeCodeForToken(code: string, codeVerifier: string): Promise<{
|
|
53
|
+
access_token: string;
|
|
54
|
+
refresh_token?: string;
|
|
55
|
+
expires_in: number;
|
|
56
|
+
token_type: string;
|
|
57
|
+
scope: string;
|
|
58
|
+
}> {
|
|
59
|
+
const params = new URLSearchParams({
|
|
60
|
+
grant_type: 'authorization_code',
|
|
61
|
+
code,
|
|
62
|
+
redirect_uri: CALLBACK_URL,
|
|
63
|
+
code_verifier: codeVerifier,
|
|
64
|
+
client_id: CLIENT_ID!,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const basicAuth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
|
|
68
|
+
|
|
69
|
+
const response = await fetch('https://api.x.com/2/oauth2/token', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
73
|
+
Authorization: `Basic ${basicAuth}`,
|
|
74
|
+
},
|
|
75
|
+
body: params.toString(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const text = await response.text();
|
|
80
|
+
throw new Error(`Token exchange failed (${response.status}): ${text}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return response.json();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fetchUserId(accessToken: string): Promise<{ id: string; username: string; name: string }> {
|
|
87
|
+
const response = await fetch('https://api.x.com/2/users/me', {
|
|
88
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
throw new Error(`Failed to fetch user info (${response.status}): ${text}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const data = await response.json() as { data: { id: string; username: string; name: string } };
|
|
97
|
+
return data.data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Main flow
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
async function main(): Promise<void> {
|
|
105
|
+
const codeVerifier = generateCodeVerifier();
|
|
106
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
107
|
+
const state = generateState();
|
|
108
|
+
|
|
109
|
+
// Build authorization URL
|
|
110
|
+
const authUrl = new URL('https://x.com/i/oauth2/authorize');
|
|
111
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
112
|
+
authUrl.searchParams.set('client_id', CLIENT_ID!);
|
|
113
|
+
authUrl.searchParams.set('redirect_uri', CALLBACK_URL);
|
|
114
|
+
authUrl.searchParams.set('scope', SCOPES.join(' '));
|
|
115
|
+
authUrl.searchParams.set('state', state);
|
|
116
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
117
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
118
|
+
|
|
119
|
+
console.log('\n=== X API OAuth 2.0 Flow ===\n');
|
|
120
|
+
console.log('Open this URL in your browser:\n');
|
|
121
|
+
console.log(authUrl.toString());
|
|
122
|
+
console.log('\nWaiting for callback on http://localhost:3000...\n');
|
|
123
|
+
|
|
124
|
+
// Try to open browser automatically
|
|
125
|
+
try {
|
|
126
|
+
const { exec } = await import('child_process');
|
|
127
|
+
const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
128
|
+
exec(`${openCmd} "${authUrl.toString()}"`);
|
|
129
|
+
} catch {
|
|
130
|
+
// Manual open is fine
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Start local server to catch callback
|
|
134
|
+
const code = await new Promise<string>((resolve, reject) => {
|
|
135
|
+
const server = http.createServer((req, res) => {
|
|
136
|
+
if (!req.url?.startsWith('/callback')) {
|
|
137
|
+
res.writeHead(404);
|
|
138
|
+
res.end('Not found');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const parsed = url.parse(req.url, true);
|
|
143
|
+
const callbackState = parsed.query.state as string;
|
|
144
|
+
const callbackCode = parsed.query.code as string;
|
|
145
|
+
const error = parsed.query.error as string;
|
|
146
|
+
|
|
147
|
+
if (error) {
|
|
148
|
+
res.writeHead(400);
|
|
149
|
+
res.end(`Authorization error: ${error}`);
|
|
150
|
+
server.close();
|
|
151
|
+
reject(new Error(`Authorization denied: ${error}`));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (callbackState !== state) {
|
|
156
|
+
res.writeHead(400);
|
|
157
|
+
res.end('State mismatch — possible CSRF. Try again.');
|
|
158
|
+
server.close();
|
|
159
|
+
reject(new Error('State mismatch'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
164
|
+
res.end(`
|
|
165
|
+
<html>
|
|
166
|
+
<body style="background: #15202B; color: #E7E9EA; font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
167
|
+
<div style="text-align: center;">
|
|
168
|
+
<h1>Authorized!</h1>
|
|
169
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
170
|
+
</div>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
173
|
+
`);
|
|
174
|
+
server.close();
|
|
175
|
+
resolve(callbackCode);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
server.listen(3000, () => {
|
|
179
|
+
// Server is ready
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Timeout after 5 minutes
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
server.close();
|
|
185
|
+
reject(new Error('Timeout — no callback received within 5 minutes'));
|
|
186
|
+
}, 300_000);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
console.log('Authorization code received. Exchanging for token...\n');
|
|
190
|
+
|
|
191
|
+
// Exchange code for token
|
|
192
|
+
const token = await exchangeCodeForToken(code, codeVerifier);
|
|
193
|
+
console.log('Token received! Fetching user info...\n');
|
|
194
|
+
|
|
195
|
+
// Fetch user ID
|
|
196
|
+
const user = await fetchUserId(token.access_token);
|
|
197
|
+
|
|
198
|
+
console.log('=== SUCCESS ===\n');
|
|
199
|
+
console.log(`Authenticated as: ${user.name} (@${user.username})`);
|
|
200
|
+
console.log(`User ID: ${user.id}`);
|
|
201
|
+
console.log(`Token expires in: ${token.expires_in}s (~${Math.round(token.expires_in / 3600)}h)`);
|
|
202
|
+
console.log(`Scopes: ${token.scope}\n`);
|
|
203
|
+
|
|
204
|
+
console.log('Add these to your .env:\n');
|
|
205
|
+
console.log(`X_USER_ACCESS_TOKEN=${token.access_token}`);
|
|
206
|
+
console.log(`X_USER_ID=${user.id}`);
|
|
207
|
+
if (token.refresh_token) {
|
|
208
|
+
console.log(`X_REFRESH_TOKEN=${token.refresh_token}`);
|
|
209
|
+
}
|
|
210
|
+
console.log('\nDone! You can now run sync.\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch((err) => {
|
|
214
|
+
console.error('OAuth flow failed:', err.message);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall hook — auto-injects x-skill context into the
|
|
4
|
+
* consuming project's CLAUDE.md so Claude Code picks it up immediately.
|
|
5
|
+
*
|
|
6
|
+
* Safe to re-run: skips if the section already exists.
|
|
7
|
+
* Only runs when installed as a dependency (not during local dev).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const MARKER = '<!-- @hidden-leaf/x-skill -->';
|
|
14
|
+
|
|
15
|
+
const SNIPPET = `
|
|
16
|
+
${MARKER}
|
|
17
|
+
## X Bookmark Intelligence (Library-Based Skill)
|
|
18
|
+
|
|
19
|
+
This project uses \`@hidden-leaf/x-skill\` — a TypeScript library for X (Twitter) bookmark intelligence.
|
|
20
|
+
This is NOT an MCP tool or registered skill. To use it, write a TypeScript script and execute it with \`npx tsx <script>.ts\`.
|
|
21
|
+
|
|
22
|
+
**Setup:** Credentials in \`.env\` — needs \`X_BEARER_TOKEN\`, \`X_USER_ID\`.
|
|
23
|
+
See node_modules/@hidden-leaf/x-skill/.env.example for all options.
|
|
24
|
+
|
|
25
|
+
**Skill reference:** Read node_modules/@hidden-leaf/x-skill/SKILL.md for the full API before using.
|
|
26
|
+
|
|
27
|
+
**How to use:** Write a .ts script, then run it:
|
|
28
|
+
\`\`\`typescript
|
|
29
|
+
import { createBookmarksSkillFromEnv } from '@hidden-leaf/x-skill';
|
|
30
|
+
const skill = createBookmarksSkillFromEnv();
|
|
31
|
+
|
|
32
|
+
// List folders: await skill.listFolders()
|
|
33
|
+
// Fetch posts: await skill.fetchByFolderName('Robotics')
|
|
34
|
+
// Research brief: await skill.brief({ folderName: 'AI MEDICINE', hlnContext: ['Applied AI Studio'] })
|
|
35
|
+
\`\`\`
|
|
36
|
+
Then execute: \`npx tsx <script>.ts\`
|
|
37
|
+
<!-- /@hidden-leaf/x-skill -->
|
|
38
|
+
`.trimStart();
|
|
39
|
+
|
|
40
|
+
function findProjectRoot() {
|
|
41
|
+
// Walk up from node_modules/@hidden-leaf/x-skill/scripts/
|
|
42
|
+
let dir = __dirname;
|
|
43
|
+
for (let i = 0; i < 10; i++) {
|
|
44
|
+
dir = path.dirname(dir);
|
|
45
|
+
const pkg = path.join(dir, 'package.json');
|
|
46
|
+
if (fs.existsSync(pkg)) {
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
49
|
+
// Don't modify our own repo
|
|
50
|
+
if (json.name === '@hidden-leaf/x-skill') continue;
|
|
51
|
+
return dir;
|
|
52
|
+
} catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function run() {
|
|
61
|
+
// Skip during local development (npm install in the skill repo itself)
|
|
62
|
+
const ownPkg = path.resolve(__dirname, '..', 'package.json');
|
|
63
|
+
if (fs.existsSync(ownPkg)) {
|
|
64
|
+
try {
|
|
65
|
+
const json = JSON.parse(fs.readFileSync(ownPkg, 'utf8'));
|
|
66
|
+
if (json.name === '@hidden-leaf/x-skill') {
|
|
67
|
+
// Check if we're in node_modules (installed as dep) or at repo root (local dev)
|
|
68
|
+
if (!__dirname.includes('node_modules')) return;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// continue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const root = findProjectRoot();
|
|
76
|
+
if (!root) return;
|
|
77
|
+
|
|
78
|
+
const claudeMd = path.join(root, 'CLAUDE.md');
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(claudeMd)) {
|
|
81
|
+
const existing = fs.readFileSync(claudeMd, 'utf8');
|
|
82
|
+
if (existing.includes(MARKER)) {
|
|
83
|
+
// Already injected — skip
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Append to existing CLAUDE.md
|
|
87
|
+
fs.writeFileSync(claudeMd, existing.trimEnd() + '\n\n' + SNIPPET);
|
|
88
|
+
console.log('[@hidden-leaf/x-skill] Added X bookmark intelligence section to CLAUDE.md');
|
|
89
|
+
console.log('[@hidden-leaf/x-skill] Restart Claude Code to activate the skill (CLAUDE.md is loaded at session start)');
|
|
90
|
+
} else {
|
|
91
|
+
// Create new CLAUDE.md
|
|
92
|
+
fs.writeFileSync(claudeMd, SNIPPET);
|
|
93
|
+
console.log('[@hidden-leaf/x-skill] Created CLAUDE.md with X bookmark intelligence');
|
|
94
|
+
console.log('[@hidden-leaf/x-skill] Restart Claude Code to activate the skill (CLAUDE.md is loaded at session start)');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Copy .env.example if no .env exists
|
|
98
|
+
const envFile = path.join(root, '.env');
|
|
99
|
+
if (!fs.existsSync(envFile)) {
|
|
100
|
+
const envExample = path.resolve(__dirname, '..', '.env.example');
|
|
101
|
+
if (fs.existsSync(envExample)) {
|
|
102
|
+
fs.copyFileSync(envExample, envFile);
|
|
103
|
+
console.log('[@hidden-leaf/x-skill] Created .env from template — fill in your X API credentials');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
run();
|
|
110
|
+
} catch {
|
|
111
|
+
// postinstall should never break npm install
|
|
112
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First sync test — pulls all bookmark folders + tweets into local cache.
|
|
3
|
+
*/
|
|
4
|
+
import { createBookmarksSkillFromEnv } from '../src/skills/bookmarks/index.js';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const skill = createBookmarksSkillFromEnv();
|
|
8
|
+
|
|
9
|
+
console.log('Syncing all bookmark folders from X API...\n');
|
|
10
|
+
|
|
11
|
+
const result = await skill.syncAll();
|
|
12
|
+
|
|
13
|
+
console.log(`Synced at: ${result.syncedAt}`);
|
|
14
|
+
console.log(`Folders: ${result.totalFolders}`);
|
|
15
|
+
console.log(`Total tweets: ${result.totalTweets}\n`);
|
|
16
|
+
|
|
17
|
+
for (const folder of result.folders) {
|
|
18
|
+
console.log(` ${folder.name} — ${folder.tweetCount} tweets`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('\nCache stats:');
|
|
22
|
+
const stats = skill.stats();
|
|
23
|
+
console.log(` Folders: ${stats.folders}`);
|
|
24
|
+
console.log(` Tweets: ${stats.tweets}`);
|
|
25
|
+
console.log(` Users: ${stats.users}`);
|
|
26
|
+
console.log(` Last sync: ${stats.lastSync}`);
|
|
27
|
+
|
|
28
|
+
skill.close();
|
|
29
|
+
console.log('\nDone!');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main().catch((err) => {
|
|
33
|
+
console.error('Sync failed:', err.message);
|
|
34
|
+
if (err.status) console.error(`Status: ${err.status}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Force-test auto-refresh by using an invalid access token.
|
|
3
|
+
* The client should detect 401, refresh using the refresh token, and succeed.
|
|
4
|
+
*/
|
|
5
|
+
import * as dotenv from 'dotenv';
|
|
6
|
+
dotenv.config();
|
|
7
|
+
import { XClient } from '../src/clients/x-client.js';
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
console.log('Force-testing auto-refresh with expired/invalid token...\n');
|
|
11
|
+
|
|
12
|
+
const client = new XClient({
|
|
13
|
+
userAccessToken: 'deliberately_invalid_token_to_trigger_401',
|
|
14
|
+
userId: process.env.X_USER_ID!,
|
|
15
|
+
refreshToken: process.env.X_REFRESH_TOKEN,
|
|
16
|
+
consumerKey: process.env.X_CONSUMER_KEY,
|
|
17
|
+
consumerSecret: process.env.X_CONSUMER_SECRET,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const me = await client.getMe();
|
|
21
|
+
|
|
22
|
+
console.log(`\nAuthenticated as: ${me.data.name} (@${me.data.username})`);
|
|
23
|
+
console.log('Auto-refresh worked — recovered from expired token!');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
main().catch((err) => {
|
|
27
|
+
console.error('Failed:', err.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test auto-refresh: calls /users/me.
|
|
3
|
+
* If token is expired, it should auto-refresh and succeed.
|
|
4
|
+
*/
|
|
5
|
+
import * as dotenv from 'dotenv';
|
|
6
|
+
dotenv.config();
|
|
7
|
+
import { createXClientFromEnv } from '../src/clients/x-client.js';
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
console.log('Testing auto-refresh...');
|
|
11
|
+
console.log(`Refresh token present: ${!!process.env.X_REFRESH_TOKEN}`);
|
|
12
|
+
console.log(`Consumer key present: ${!!process.env.X_CONSUMER_KEY}`);
|
|
13
|
+
console.log(`Consumer secret present: ${!!process.env.X_CONSUMER_SECRET}\n`);
|
|
14
|
+
|
|
15
|
+
const client = createXClientFromEnv();
|
|
16
|
+
const me = await client.getMe();
|
|
17
|
+
|
|
18
|
+
console.log(`Authenticated as: ${me.data.name} (@${me.data.username})`);
|
|
19
|
+
console.log('Auto-refresh is working!');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main().catch((err) => {
|
|
23
|
+
console.error('Failed:', err.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|