fieldtheory-cli-windowsport 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 +21 -0
- package/README.md +161 -0
- package/bin/ft.mjs +15 -0
- package/dist/bookmark-classify-llm.js +247 -0
- package/dist/bookmark-classify.js +223 -0
- package/dist/bookmark-media.js +186 -0
- package/dist/bookmarks-db.js +644 -0
- package/dist/bookmarks-service.js +49 -0
- package/dist/bookmarks-viz.js +597 -0
- package/dist/bookmarks.js +190 -0
- package/dist/chrome-cookies.js +239 -0
- package/dist/cli.js +642 -0
- package/dist/command-path.js +58 -0
- package/dist/config.js +54 -0
- package/dist/db.js +33 -0
- package/dist/fs.js +45 -0
- package/dist/graphql-bookmarks.js +398 -0
- package/dist/paths.js +43 -0
- package/dist/types.js +1 -0
- package/dist/xauth.js +135 -0
- package/package.json +63 -0
package/dist/xauth.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { URL } from 'node:url';
|
|
4
|
+
import { pathExists, readJson, writeJson } from './fs.js';
|
|
5
|
+
import { ensureDataDir, twitterOauthTokenPath } from './paths.js';
|
|
6
|
+
import { loadXApiConfig } from './config.js';
|
|
7
|
+
function base64Url(input) {
|
|
8
|
+
return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
9
|
+
}
|
|
10
|
+
function createPkce() {
|
|
11
|
+
const verifier = base64Url(crypto.randomBytes(32));
|
|
12
|
+
const challenge = base64Url(crypto.createHash('sha256').update(verifier).digest());
|
|
13
|
+
const state = base64Url(crypto.randomBytes(16));
|
|
14
|
+
return { verifier, challenge, state };
|
|
15
|
+
}
|
|
16
|
+
export function buildTwitterOAuthUrl() {
|
|
17
|
+
const cfg = loadXApiConfig();
|
|
18
|
+
if (!cfg.callbackUrl) {
|
|
19
|
+
throw new Error('Missing X_CALLBACK_URL in .env.local');
|
|
20
|
+
}
|
|
21
|
+
const { verifier, challenge, state } = createPkce();
|
|
22
|
+
const url = new URL('https://twitter.com/i/oauth2/authorize');
|
|
23
|
+
url.searchParams.set('response_type', 'code');
|
|
24
|
+
url.searchParams.set('client_id', cfg.clientId);
|
|
25
|
+
url.searchParams.set('redirect_uri', cfg.callbackUrl);
|
|
26
|
+
url.searchParams.set('scope', 'tweet.read users.read bookmark.read offline.access');
|
|
27
|
+
url.searchParams.set('state', state);
|
|
28
|
+
url.searchParams.set('code_challenge', challenge);
|
|
29
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
30
|
+
return { url: url.toString(), state, verifier };
|
|
31
|
+
}
|
|
32
|
+
async function exchangeCodeForToken(code, verifier) {
|
|
33
|
+
const cfg = loadXApiConfig();
|
|
34
|
+
if (!cfg.callbackUrl) {
|
|
35
|
+
throw new Error('Missing X_CALLBACK_URL in .env.local');
|
|
36
|
+
}
|
|
37
|
+
const basic = Buffer.from(`${cfg.clientId}:${cfg.clientSecret}`).toString('base64');
|
|
38
|
+
const body = new URLSearchParams({
|
|
39
|
+
grant_type: 'authorization_code',
|
|
40
|
+
code,
|
|
41
|
+
redirect_uri: cfg.callbackUrl,
|
|
42
|
+
code_verifier: verifier,
|
|
43
|
+
client_id: cfg.clientId,
|
|
44
|
+
});
|
|
45
|
+
const response = await fetch('https://api.x.com/2/oauth2/token', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Basic ${basic}`,
|
|
49
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
50
|
+
},
|
|
51
|
+
body,
|
|
52
|
+
});
|
|
53
|
+
const text = await response.text();
|
|
54
|
+
const parsed = JSON.parse(text);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`Token exchange failed (HTTP ${response.status}). Check your X API credentials.`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
access_token: parsed.access_token,
|
|
60
|
+
refresh_token: parsed.refresh_token,
|
|
61
|
+
expires_in: parsed.expires_in,
|
|
62
|
+
scope: parsed.scope,
|
|
63
|
+
token_type: parsed.token_type,
|
|
64
|
+
obtained_at: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function saveTwitterOAuthToken(token) {
|
|
68
|
+
ensureDataDir();
|
|
69
|
+
const tokenPath = twitterOauthTokenPath();
|
|
70
|
+
await writeJson(tokenPath, token);
|
|
71
|
+
// Restrict permissions — OAuth tokens should only be readable by the owner
|
|
72
|
+
const { chmod } = await import('node:fs/promises');
|
|
73
|
+
await chmod(tokenPath, 0o600);
|
|
74
|
+
return tokenPath;
|
|
75
|
+
}
|
|
76
|
+
export async function loadTwitterOAuthToken() {
|
|
77
|
+
const tokenPath = twitterOauthTokenPath();
|
|
78
|
+
if (!(await pathExists(tokenPath)))
|
|
79
|
+
return null;
|
|
80
|
+
return readJson(tokenPath);
|
|
81
|
+
}
|
|
82
|
+
export async function runTwitterOAuthFlow() {
|
|
83
|
+
const cfg = loadXApiConfig();
|
|
84
|
+
if (!cfg.callbackUrl) {
|
|
85
|
+
throw new Error('Missing X_CALLBACK_URL in .env.local');
|
|
86
|
+
}
|
|
87
|
+
const { url, state, verifier } = buildTwitterOAuthUrl();
|
|
88
|
+
const callback = new URL(cfg.callbackUrl);
|
|
89
|
+
const port = Number(callback.port || 80);
|
|
90
|
+
const pathname = callback.pathname;
|
|
91
|
+
const code = await new Promise((resolve, reject) => {
|
|
92
|
+
const server = http.createServer((req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const reqUrl = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
95
|
+
if (reqUrl.pathname !== pathname) {
|
|
96
|
+
res.statusCode = 404;
|
|
97
|
+
res.end('Not found');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const returnedState = reqUrl.searchParams.get('state');
|
|
101
|
+
const returnedCode = reqUrl.searchParams.get('code');
|
|
102
|
+
const error = reqUrl.searchParams.get('error');
|
|
103
|
+
if (error) {
|
|
104
|
+
res.statusCode = 400;
|
|
105
|
+
res.end(`OAuth error: ${error}`);
|
|
106
|
+
server.close();
|
|
107
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!returnedCode || returnedState !== state) {
|
|
111
|
+
res.statusCode = 400;
|
|
112
|
+
res.end('Invalid OAuth callback');
|
|
113
|
+
server.close();
|
|
114
|
+
reject(new Error('Invalid OAuth callback state/code'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
res.statusCode = 200;
|
|
118
|
+
res.end('ftx auth complete. You can close this tab.');
|
|
119
|
+
server.close();
|
|
120
|
+
resolve(returnedCode);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
server.close();
|
|
124
|
+
reject(err);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
server.listen(port, '127.0.0.1', () => {
|
|
128
|
+
console.log('Open this URL in your browser to authorize X bookmarks access:');
|
|
129
|
+
console.log(url);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
const token = await exchangeCodeForToken(code, verifier);
|
|
133
|
+
const tokenPath = await saveTwitterOAuthToken(token);
|
|
134
|
+
return { tokenPath, scope: token.scope };
|
|
135
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fieldtheory-cli-windowsport",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "FieldTheory for Windows. Inspired by Andrew Farah's Field Theory CLI for local X/Twitter bookmark sync, search, and classification.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ftx": "bin/ft.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
12
|
+
"dev": "tsx src/cli.ts",
|
|
13
|
+
"start": "node dist/cli.js",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"dist/",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/shangobashi/fieldtheory-cli-windowsport.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/shangobashi/fieldtheory-cli-windowsport#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/shangobashi/fieldtheory-cli-windowsport/issues"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"bookmarks",
|
|
40
|
+
"twitter",
|
|
41
|
+
"x",
|
|
42
|
+
"windows",
|
|
43
|
+
"codex",
|
|
44
|
+
"search",
|
|
45
|
+
"fts5",
|
|
46
|
+
"sqlite",
|
|
47
|
+
"local-first",
|
|
48
|
+
"self-custody",
|
|
49
|
+
"cli"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"commander": "^14.0.3",
|
|
53
|
+
"dotenv": "^17.3.1",
|
|
54
|
+
"sql.js": "^1.14.1",
|
|
55
|
+
"sql.js-fts5": "^1.4.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^25.5.0",
|
|
59
|
+
"@types/sql.js": "^1.4.11",
|
|
60
|
+
"tsx": "^4.21.0",
|
|
61
|
+
"typescript": "^6.0.2"
|
|
62
|
+
}
|
|
63
|
+
}
|