@harness.farm/social-cli 0.1.0 โ†’ 0.1.2

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,75 @@
1
+ /**
2
+ * Runner โ€” orchestrates login flow + command execution for any adapter.
3
+ *
4
+ * Flow:
5
+ * 1. Connect to Chrome tab
6
+ * 2. Check if already logged in (via saved session or live cookies)
7
+ * 3. If not, navigate to loginUrl and wait for user to log in
8
+ * 4. Save session cookies
9
+ * 5. Run the requested command
10
+ * 6. Render output
11
+ */
12
+ import { newTab, sleep } from './cdp.js';
13
+ import { captureSession, restoreSession, hasSession } from './session.js';
14
+ import { renderTable } from '../output/table.js';
15
+ export async function run(adapter, opts) {
16
+ const port = opts.cdpPort ?? 9222;
17
+ console.log(`\n๐Ÿ”Œ ่ฟžๆŽฅ Chrome CDP :${port}...`);
18
+ const client = await newTab(port);
19
+ // --- ็™ปๅฝ•ๆต็จ‹ ---
20
+ let loggedIn = false;
21
+ // ๅ…ˆๅฐ่ฏ•ๆขๅคๅทฒไฟๅญ˜็š„ session
22
+ if (hasSession(adapter.platform)) {
23
+ await restoreSession(client, adapter.platform);
24
+ // ๅฏผ่ˆชๅˆฐ็›ฎๆ ‡็ซ™้ชŒ่ฏ session ๆ˜ฏๅฆ่ฟ˜ๆœ‰ๆ•ˆ
25
+ await client.navigate(adapter.loginUrl, 3000);
26
+ loggedIn = await adapter.isLoggedIn(client);
27
+ if (!loggedIn) {
28
+ console.log('โš ๏ธ ๅทฒไฟๅญ˜็š„ session ๅทฒๅคฑๆ•ˆ๏ผŒ้œ€่ฆ้‡ๆ–ฐ็™ปๅฝ•');
29
+ }
30
+ }
31
+ // session ๆ— ๆ•ˆๆˆ–ๆฒกๆœ‰ session โ†’ ๅผ•ๅฏผ็”จๆˆท็™ปๅฝ•
32
+ if (!loggedIn) {
33
+ await client.navigate(adapter.loginUrl, 2000);
34
+ console.log(`\n๐Ÿ”‘ ่ฏทๅœจ Chrome ไธญ็™ปๅฝ• ${adapter.platform}๏ผŒๅฎŒๆˆๅŽๆŒ‰ Enter ็ปง็ปญ...`);
35
+ await waitForEnter();
36
+ // ็ญ‰ๅพ…ๅนถ่ฝฎ่ฏข็™ปๅฝ•็Šถๆ€
37
+ for (let i = 0; i < 30; i++) {
38
+ loggedIn = await adapter.isLoggedIn(client);
39
+ if (loggedIn)
40
+ break;
41
+ process.stdout.write(`\r็ญ‰ๅพ…็™ปๅฝ•... ${(i + 1) * 2}s`);
42
+ await sleep(2000);
43
+ }
44
+ process.stdout.write('\n');
45
+ if (!loggedIn) {
46
+ client.close();
47
+ throw new Error('็™ปๅฝ•่ถ…ๆ—ถ๏ผŒ่ฏท้‡่ฏ•');
48
+ }
49
+ // ไฟๅญ˜ session
50
+ await captureSession(client, adapter.platform);
51
+ }
52
+ console.log(`โœ… ๅทฒ็™ปๅฝ• ${adapter.platform}`);
53
+ // --- ๆ‰ง่กŒๅ‘ฝไปค ---
54
+ const handler = adapter.commands[opts.command];
55
+ if (!handler) {
56
+ const available = Object.keys(adapter.commands).join(', ');
57
+ client.close();
58
+ throw new Error(`ๆœช็Ÿฅๅ‘ฝไปค "${opts.command}"๏ผŒๅฏ็”จ: ${available}`);
59
+ }
60
+ console.log(`\n๐Ÿ” ๆ‰ง่กŒ: ${adapter.platform} ${opts.command} ${(opts.args ?? []).join(' ')}\n`);
61
+ const result = await handler(client, opts.args ?? []);
62
+ client.close();
63
+ // --- ๆธฒๆŸ“ ---
64
+ renderTable(result.columns, result.rows);
65
+ }
66
+ function waitForEnter() {
67
+ return new Promise((resolve) => {
68
+ process.stdin.setRawMode?.(false);
69
+ process.stdin.resume();
70
+ process.stdin.once('data', () => {
71
+ process.stdin.pause();
72
+ resolve();
73
+ });
74
+ });
75
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Session manager โ€” save/load cookies to disk per platform.
3
+ */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ const SESSION_DIR = path.join(process.env.HOME ?? '.', '.cdp-scraper', 'sessions');
7
+ function sessionPath(platform) {
8
+ return path.join(SESSION_DIR, `${platform}.json`);
9
+ }
10
+ export function saveSession(platform, cookies) {
11
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
12
+ fs.writeFileSync(sessionPath(platform), JSON.stringify(cookies, null, 2));
13
+ console.log(`โœ… Session saved โ†’ ${sessionPath(platform)} (${cookies.length} cookies)`);
14
+ }
15
+ export function loadSession(platform) {
16
+ const p = sessionPath(platform);
17
+ if (!fs.existsSync(p))
18
+ return null;
19
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
20
+ }
21
+ export function hasSession(platform) {
22
+ return fs.existsSync(sessionPath(platform));
23
+ }
24
+ /** ไปŽ browser ่ฏปๅ– cookies ๅนถไฟๅญ˜ */
25
+ export async function captureSession(client, platform) {
26
+ const cookies = await client.getAllCookies();
27
+ saveSession(platform, cookies);
28
+ return cookies;
29
+ }
30
+ /** ๆขๅคๅทฒไฟๅญ˜็š„ session ๅˆฐ browser */
31
+ export async function restoreSession(client, platform) {
32
+ const cookies = loadSession(platform);
33
+ if (!cookies)
34
+ return false;
35
+ await client.setCookies(cookies);
36
+ console.log(`โœ… Session restored โ† ${platform} (${cookies.length} cookies)`);
37
+ return true;
38
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry โ€” supports both YAML adapters and TypeScript adapters.
4
+ *
5
+ * Usage:
6
+ * tsx src/cli.ts <platform> <command> [args...]
7
+ *
8
+ * Adapter resolution order:
9
+ * 1. adapters/<platform>.yaml โ† YAML-first
10
+ * 2. src/adapters/<platform>.ts โ† TypeScript fallback
11
+ *
12
+ * Examples:
13
+ * tsx src/cli.ts xhs search ๆณ•ๅพ‹ai
14
+ * tsx src/cli.ts xhs like "https://..."
15
+ * tsx src/cli.ts xhs comment "https://..." "ๅคชๆฃ’ไบ†๏ผ"
16
+ * tsx src/cli.ts xhs post --title "ๆ ‡้ข˜" --content "ๅ†…ๅฎน"
17
+ */
18
+ import path from 'path';
19
+ import fs from 'fs';
20
+ import { fileURLToPath } from 'url';
21
+ import { runYamlCommand } from './runner/yaml-runner.js';
22
+ import { run } from './browser/runner.js';
23
+ import { adapters } from './adapters/index.js';
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const ROOT = path.resolve(__dirname, '..');
26
+ const [, , platform, command, ...rest] = process.argv;
27
+ if (!platform || !command) {
28
+ printHelp();
29
+ process.exit(1);
30
+ }
31
+ // โ”€โ”€ Resolve adapter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
32
+ const yamlPath = path.join(ROOT, 'adapters', `${platform}.yaml`);
33
+ const hasYaml = fs.existsSync(yamlPath);
34
+ const tsAdapter = adapters[platform];
35
+ if (!hasYaml && !tsAdapter) {
36
+ console.error(`โŒ Unknown platform "${platform}"`);
37
+ console.error(` YAML adapters: ${listYamlAdapters().join(', ') || '(none)'}`);
38
+ console.error(` TS adapters: ${Object.keys(adapters).join(', ')}`);
39
+ process.exit(1);
40
+ }
41
+ // โ”€โ”€ Parse args โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
42
+ // Support both positional args and --key value flags
43
+ const args = parseArgs(rest);
44
+ // โ”€โ”€ Run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
45
+ if (hasYaml) {
46
+ // YAML adapter: pass positional args in order
47
+ runYamlCommand(yamlPath, command, args.positional).catch(err => {
48
+ console.error('โŒ', err.message);
49
+ process.exit(1);
50
+ });
51
+ }
52
+ else {
53
+ // TypeScript adapter: pass flags as array (legacy)
54
+ run(tsAdapter, { command, args: rest }).catch(err => {
55
+ console.error('โŒ', err.message);
56
+ process.exit(1);
57
+ });
58
+ }
59
+ // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
60
+ function parseArgs(argv) {
61
+ const positional = [];
62
+ const flags = {};
63
+ for (let i = 0; i < argv.length; i++) {
64
+ if (argv[i].startsWith('--')) {
65
+ flags[argv[i].slice(2)] = argv[++i] ?? '';
66
+ }
67
+ else {
68
+ positional.push(argv[i]);
69
+ }
70
+ }
71
+ return { positional, flags };
72
+ }
73
+ function listYamlAdapters() {
74
+ const dir = path.join(ROOT, 'adapters');
75
+ if (!fs.existsSync(dir))
76
+ return [];
77
+ return fs.readdirSync(dir)
78
+ .filter(f => f.endsWith('.yaml'))
79
+ .map(f => f.replace('.yaml', ''));
80
+ }
81
+ function printHelp() {
82
+ const yaml = listYamlAdapters();
83
+ const ts = Object.keys(adapters);
84
+ console.log('็”จๆณ•: tsx src/cli.ts <platform> <command> [args...]');
85
+ console.log('');
86
+ if (yaml.length) {
87
+ console.log('YAML ๅนณๅฐ (ๆŽจ่):');
88
+ yaml.forEach(p => console.log(` ${p}`));
89
+ }
90
+ if (ts.length) {
91
+ console.log('TS ๅนณๅฐ:');
92
+ ts.forEach(p => console.log(` ${p}`));
93
+ }
94
+ console.log('');
95
+ console.log('็คบไพ‹:');
96
+ console.log(' tsx src/cli.ts xhs search ๆณ•ๅพ‹ai');
97
+ console.log(' tsx src/cli.ts xhs like "https://www.xiaohongshu.com/explore/..."');
98
+ console.log(' tsx src/cli.ts xhs comment "https://..." "ๅคชๆฃ’ไบ†๏ผ"');
99
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * CLI table renderer with CJK character width support.
3
+ */
4
+ function dispWidth(s) {
5
+ let w = 0;
6
+ for (const c of s)
7
+ w += c.codePointAt(0) > 127 ? 2 : 1;
8
+ return w;
9
+ }
10
+ function truncate(s, maxW) {
11
+ let out = '', cur = 0;
12
+ for (const c of s) {
13
+ const cw = c.codePointAt(0) > 127 ? 2 : 1;
14
+ if (cur + cw > maxW - 1)
15
+ return out + 'โ€ฆ';
16
+ out += c;
17
+ cur += cw;
18
+ }
19
+ return out;
20
+ }
21
+ function pad(s, width) {
22
+ return s + ' '.repeat(Math.max(0, width - dispWidth(s)));
23
+ }
24
+ function sep(cols, char = '-') {
25
+ return '+' + cols.map((c) => char.repeat(c.width + 2)).join('+') + '+';
26
+ }
27
+ function row(cols, values) {
28
+ return '|' + cols.map((c, i) => ` ${pad(truncate(String(values[i] ?? ''), c.width), c.width)} `).join('|') + '|';
29
+ }
30
+ export function renderTable(cols, data) {
31
+ const divider = sep(cols);
32
+ const doubleSep = sep(cols, '=');
33
+ console.log(divider);
34
+ console.log(row(cols, cols.map((c) => c.header)));
35
+ console.log(doubleSep);
36
+ data.forEach((item, i) => {
37
+ console.log(row(cols, cols.map((c) => String(item[c.key] ?? ''))));
38
+ if (i < data.length - 1)
39
+ console.log(divider);
40
+ });
41
+ console.log(divider);
42
+ console.log(`\nๅ…ฑ ${data.length} ๆก็ป“ๆžœ`);
43
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Step executor โ€” maps YAML step types to agent-browser CLI calls.
3
+ *
4
+ * Each method runs `agent-browser <args>` and returns parsed JSON result.
5
+ */
6
+ import { execSync } from 'child_process';
7
+ export class StepExecutor {
8
+ wsUrl;
9
+ connected = false;
10
+ constructor(wsUrl) {
11
+ this.wsUrl = wsUrl;
12
+ }
13
+ /** Connect to Chrome tab (once per session) */
14
+ connect() {
15
+ if (this.connected)
16
+ return;
17
+ this.run(['connect', this.wsUrl]);
18
+ this.connected = true;
19
+ }
20
+ // โ”€โ”€ Core step methods โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
21
+ open(url) {
22
+ return this.run(['open', url]);
23
+ }
24
+ click(selector) {
25
+ return this.run(['click', selector]);
26
+ }
27
+ /** Click an element by its visible text (uses eval under the hood) */
28
+ clickText(text) {
29
+ const js = `(function(){
30
+ var el = [...document.querySelectorAll('*')].find(function(e){
31
+ return e.textContent.trim() === ${JSON.stringify(text)} && e.children.length === 0;
32
+ });
33
+ if(el){ el.click(); return true; }
34
+ return false;
35
+ })()`;
36
+ const r = this.eval(js);
37
+ if (!r.value)
38
+ return { ok: false, error: `Text not found: "${text}"` };
39
+ return { ok: true };
40
+ }
41
+ fill(selector, value) {
42
+ // Use eval to avoid shell-quoting issues with complex selectors
43
+ const js = `(function(){
44
+ var el = document.querySelector(${JSON.stringify(selector)});
45
+ if (!el) return false;
46
+ el.focus();
47
+ var nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
48
+ || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');
49
+ if (nativeSet && nativeSet.set) nativeSet.set.call(el, ${JSON.stringify(value)});
50
+ else el.value = ${JSON.stringify(value)};
51
+ el.dispatchEvent(new Event('input', { bubbles: true }));
52
+ el.dispatchEvent(new Event('change', { bubbles: true }));
53
+ return true;
54
+ })()`;
55
+ return this.eval(js);
56
+ }
57
+ type(selector, value) {
58
+ return this.run(['type', selector, value]);
59
+ }
60
+ /** Type into a contenteditable element via execCommand */
61
+ typeContentEditable(selector, value) {
62
+ const js = `(function(){
63
+ var el = document.querySelector(${JSON.stringify(selector)});
64
+ if(!el) return false;
65
+ el.focus();
66
+ document.execCommand('selectAll', false, null);
67
+ document.execCommand('insertText', false, ${JSON.stringify(value)});
68
+ return el.textContent || el.value || true;
69
+ })()`;
70
+ return this.eval(js);
71
+ }
72
+ /** Type via agent-browser's real keystroke simulation (works with Draft.js / React) */
73
+ typeKeys(selector, value) {
74
+ return this.run(['type', selector, value]);
75
+ }
76
+ wait(msOrSelector) {
77
+ if (typeof msOrSelector === 'number') {
78
+ return this.run(['wait', String(msOrSelector)]);
79
+ }
80
+ return this.run(['wait', msOrSelector]);
81
+ }
82
+ /** Press a key via agent-browser press (real key event, e.g. z, x, Enter, Control+Enter) */
83
+ pressKey(key) {
84
+ return this.run(['press', key]);
85
+ }
86
+ /** Insert text into the currently focused element via agent-browser keyboard type */
87
+ keyboardInsertText(text) {
88
+ // 'keyboard type' sends real key events char-by-char โ€” works with Draft.js
89
+ return this.run(['keyboard', 'type', text]);
90
+ }
91
+ eval(js) {
92
+ const r = this.run(['eval', js]);
93
+ if (r.ok && r.value !== undefined) {
94
+ // agent-browser wraps eval result in data.result
95
+ const data = r.value;
96
+ return { ok: true, value: data.result ?? r.value };
97
+ }
98
+ return r;
99
+ }
100
+ screenshot(path) {
101
+ return this.run(path ? ['screenshot', path] : ['screenshot']);
102
+ }
103
+ upload(selector, filePath) {
104
+ return this.run(['upload', selector, filePath]);
105
+ }
106
+ getUrl() {
107
+ const r = this.run(['get', 'url']);
108
+ const data = r.value;
109
+ return data?.url ?? '';
110
+ }
111
+ snapshot() {
112
+ const r = this.run(['snapshot']);
113
+ return String(r.value ?? '');
114
+ }
115
+ // โ”€โ”€ Internal runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
116
+ run(args) {
117
+ try {
118
+ const cmd = `agent-browser ${args.map(a => this.shellQuote(a)).join(' ')}`;
119
+ const out = execSync(cmd, {
120
+ env: { ...process.env, AGENT_BROWSER_JSON: '1' },
121
+ timeout: 30000,
122
+ encoding: 'utf8',
123
+ });
124
+ // Find last JSON line (agent-browser may emit warnings before JSON)
125
+ const jsonLine = out.trim().split('\n').reverse().find(l => l.startsWith('{'));
126
+ if (!jsonLine)
127
+ return { ok: true };
128
+ const parsed = JSON.parse(jsonLine);
129
+ if (!parsed.success)
130
+ return { ok: false, error: parsed.error ?? 'unknown error' };
131
+ return { ok: true, value: parsed.data };
132
+ }
133
+ catch (err) {
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ return { ok: false, error: msg };
136
+ }
137
+ }
138
+ shellQuote(s) {
139
+ // Wrap in single quotes, escape internal single quotes
140
+ return `'${s.replace(/'/g, "'\\''")}'`;
141
+ }
142
+ }